view release on metacpan or search on metacpan
.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-release-checker.md view on Meta::CPAN
dependency. Known state at the time of writing: `URI` is declared and used nowhere;
`IO::Socket::INET` is loaded by `Role::HTTP` and undeclared while its `::UNIX` sibling
is declared; `Carp` is loaded everywhere and undeclared. Say which of those are worth
changing; do not leave them unmentioned.
2. **`$VERSION` consistency** â `grep -rn 'our \$VERSION' lib` must return the same
literal for all 12 modules. A module carrying a stale version, or a new module with
none, is a release blocker. The value is the *next* release; the previous one is the
last git tag.
3. **`dist.ini`** â `[@Author::GETTY]`, `copyright_year` current.
4. **`Changes`** â a `{{$NEXT}}` section exists and covers the user-visible changes since
the last tag (`git log --oneline $(git describe --tags --abbrev=0 2>/dev/null)..`).
The house standard here is a measured claim, not a summary: existing entries name the
engine's exact error string and the observed before/after. Flag an entry that asserts
daemon behavior without evidence.
5. **POD in sync with the code.** Every public method has `=method`, every attribute
`=attr`, and the option lists match what the method actually forwards â the drift most
likely to ship is an option added to a `%params` block and never documented. Check
`README.md` against `lib/API/Docker.pm`'s SYNOPSIS too.
6. **`dzil build`** â clean, no missing files, no warnings; then `dzil test` green,
including the generated `xt/` author and release tests (pod-syntax,
changes_has_content).
.claude/agents/api-docker-test-writer.md view on Meta::CPAN
return value proves the fixture, not the code.
- **Decode exactly what the engine would receive.** The push-auth helper used to append
the missing base64 padding before decoding and so passed with and without the defect
it existed to catch. Never normalise the value under test on the way into the
assertion.
- **Live and mock must both be able to pass, or the assertion is gated.** `test_docker`
ignores the route table entirely under `API_DOCKER_TEST_HOST` â an assertion tied to
fixture contents runs against a real daemon's data otherwise.
New fixtures are captured from a real daemon into `t/fixtures/*.json`, never hand-rolled
â and that includes wire formats. A test for the multiplexed log stream asserts against
bytes the engine actually produced, not against a frame header written from memory; the
Engine API reference above tells you what to expect, the socket tells you what is true.
Test filenames follow the existing flat, topical naming (`t/images.t`,
`t/images_push_auth.t`), one file per resource or per defect.
A test asserts intent: it must be able to fail when the logic changes. Reproduce a bug
before fixing it and leave the regression behind. Verify with `prove -lr t/`; a single
file with `prove -lv t/NN.t`.
.claude/agents/api-docker-worker.md view on Meta::CPAN
above are non-negotiable â apply silently, do not restate.
**Where your lane ends.** You own how this distribution is built: `Role::HTTP`'s socket
handling and chunked reader, Moo composition, the entity roles composed onto the
generated types, `cpanfile`, dist
plumbing. You do not own what the Docker Engine accepts or answers. If the task turns on
daemon semantics â a wire format, a query-parameter meaning, a response shape, registry
auth, API version gating â stop and hand it to `api-docker-engine-worker`, which is
briefed with the Engine API reference and you are not. Guessing at daemon behavior from
the existing Perl is exactly how a wrong assumption gets cemented; the current
`containers->logs` is the proof.
Coordinate via `karr`: pick tickets from the local board, record drift you find as new
tickets rather than expanding scope mid-change.
## Repo-specific notes â beyond the briefed skills
**This distribution has a consumer in the same workspace.**
`../p5-dist-zilla-plugin-docker-api` calls into `$docker->images` and the client
constructor. Changing a public method signature, a return type, or what `_request` hands
back is a cross-repo change: check that repo builds and tests green, or file a ticket on
.claude/rules/api-docker-rules.md view on Meta::CPAN
1. **Think before coding** â state assumptions; when uncertain, ask rather than guess.
Push back when a simpler approach exists.
2. **Simplicity first** â minimum code that solves the problem. Nothing speculative.
3. **Surgical changes** â touch only what you must. Match existing style.
4. **Goal-driven execution** â define success criteria, loop until verified.
5. **Surface conflicts, don't average them** â pick one (more recent / more tested), flag
the other for cleanup. Don't blend.
6. **Read before you write** â `Role::HTTP` is the single seam every resource API and
entity class hangs off. A change to `_request`'s options, return shape or error
handling reaches every module in `lib/` and the mock harness at once.
7. **Tests verify intent, not just behavior** â a test that can't fail when the logic
changes is wrong, and a helper that normalises its input before asserting is that
test. Reproduce a bug before fixing it; leave the regression behind.
8. **Checkpoint after every significant step** â summarize: done / verified / left.
9. **Match conventions** â conformance > taste. Surface a harmful convention; don't fork
silently.
10. **Fail loud** â "Done" is wrong if anything was skipped. "Tests pass" is wrong if any
were skipped â and in this repo a skip is the default failure mode, see below.
11. **A red test is a claim before it is a failure** â before changing code to turn a
test green, say what the test asserts and whether your fix keeps that claim or
replaces it. If the claim is wrong, fix the claim and say so.
.claude/skills/api-docker-core/SKILL.md view on Meta::CPAN
=> $self->client)` â never `new`. A daemon response and a caller-built
object are different name spaces: `from_data` reads only the swagger's own
wire names, so a key it does not recognise keeps its own spelling in
`unknown_fields` instead of being misread as the Perl name of an unrelated
field it happens to collide with, and a value that disagrees with the
declared type costs that one field (recorded in `rejected_fields`) rather
than failing the whole response. `new` stays strict and croaks on both
cases â it is what a caller's own arguments go through. Detail:
`API::Docker::Role::Type`.
The convenience methods (`$container->start`, `$image->remove`, `logs`,
`is_running`, ...) are not on the generated classes. They live in
`API::Docker::Role::Entity::*` roles and are composed onto the generated
`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`,
.claude/skills/api-docker-core/SKILL.md view on Meta::CPAN
pad. `_registry_auth_header` produces padded base64url (`tr{+/}{-_}`, no `=`
removal); a bare base64-looking string passed as `auth` is forwarded
untouched.
## Transport behavior that's easy to get wrong
- **Buffers the whole response by default; streaming needs a callback.**
Every request sends `Connection: close`. With none of `on_event`,
`on_frame`, `on_chunk` given, `_request` reads the whole response before
parsing, so `/build`, `/images/create`, `/push`, `/events`,
`/containers/*/stats` and `logs(follow)` block until the daemon closes the
connection â an unbounded `events` or `stats` call without one of those
callbacks never returns. Pass one of the three to consume the response as
it arrives instead. Detail: `API::Docker::Role::HTTP`'s "Streaming a
response as it arrives".
- **The buffered streaming return type is not stable.** `_request` first
tries `decode_json` on the whole body and only falls back to line-by-line
NDJSON parsing (returning an arrayref of events). A stream that carries
exactly one JSON object comes back as that hashref, not as a one-element
array. Callers check `ref` before iterating.
- **A failed build/pull/push is still HTTP 200.** `_request` croaks on status
.claude/skills/docker-engine-api/SKILL.md view on Meta::CPAN
---
name: docker-engine-api
description: "Use when talking to the Docker Engine HTTP API directly â writing or debugging a client, hitting /containers, /images, /build, /exec, /events over the socket, curl --unix-socket probes, garbled log output, a 400 on push, filters that ...
---
# Docker Engine HTTP API
For code that speaks the API over a socket rather than shelling out to `docker`.
The CLI hides everything below; a client has to handle it. Endpoint lists live
in the daemon's own reference â what follows is what the reference states once
and clients get wrong repeatedly.
## Versioning
.claude/skills/docker-engine-api/SKILL.md view on Meta::CPAN
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
`POST /exec/{id}/start` return **frames, not text**, whenever the container was
created **without** a TTY:
```
[STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4][payload of SIZE bytes]
```
`STREAM_TYPE` is 0 stdin, 1 stdout, 2 stderr. `SIZE` is a big-endian uint32.
Frames repeat until the stream ends. Measured against a container running
`echo OUT; echo ERR 1>&2`:
```
Tty=0: 01 00 00 00 00 00 00 04 "OUT\n" 02 00 00 00 00 00 00 04 "ERR\n"
Tty=1: "OUT\r\n" "ERR\r\n"
```
**With `Tty: true` the stream is raw** â no headers, and newlines arrive as
`\r\n` because a PTY is involved. That is the trap: a developer testing by hand
reaches for an interactive container, sees clean text, and ships a client that
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
.claude/skills/getty-git-commit-style/SKILL.md view on Meta::CPAN
Update driver code and documentation
```
Bad (too verbose):
```
This commit updates the Sybase driver distribution to use the new
[DBIO] Dist::Zilla plugin bundle instead of the previous [Author::GETTY]
bundle. Additionally, it fixes an issue where...
```
## Changelog entries
Where the repo carries a `Changes` or `CHANGELOG`, the entry belongs in the same commit
as the change it describes, and the rules above apply to it unchanged. One thing makes
it harder than a commit message: a message is written once and never seen again, while
the unreleased section stays open for weeks and is edited again every time the same
area is touched.
**One topic, one entry.** Before writing a bullet, read the unreleased section for the
topic you are about to describe. If it already has one, **rewrite that bullet** to say
where the code now stands â never append a second. Three bullets circling one option
are three chances to contradict each other, and the reader wants the released state,
not the sequence of attempts that produced it.
**Describe the destination, not the journey.** No "used to", no "previously", no
walkthrough of the mechanism, no defence of the alternative that lost. Name what is
true now and what a user does differently because of it. The reasoning has homes that
keep it â the commit body, the ticket, an ADR; a changelog is read by someone who
never saw the old behaviour.
**Reference only the tracker the repo publishes.** In a changelog a `#123` earns its
place when the reader can open it â GitHub or Gitea issues on a repo that has them. An
internal board ticket is unreadable outside the workspace, so it does not appear there
at all: name the change instead of the number. A commit message may carry one, written
in the board's own notation (karr ids are `k254`) â never as `#254`, which every
hosting platform resolves against *its* issue 254: a dead link today, someone else's
bug once the repo has that many.
Aim for one to three lines per entry, and let the count of entries fall out of the
work rather than the detail per entry. For scale: a mature Getty distribution carries
320 releases in a 348-line `Changes` â a whole history shorter than a single unreleased
.claude/skills/getty-perl-core/SKILL.md view on Meta::CPAN
- **Types:** the tendency is to type what arrives from outside â Moose's own constraints where Moose is already there, `Types::Standard` where it is not. Not every distribution needs a type system, and none needs one for every field: `getty-perl-ty...
## Singletons
- **`->instance`** for `MooseX::Singleton` / `MooX::Singleton` classes. Never `->new` on a singleton.
- **`->new`** for everything else.
## Subroutines
- **`my ( $self, $x ) = @_;`** as the first line â explicit destructure, spaces inside the parens. Never `my $self = shift;` as argument unpacking.
- **One-liners skip unpacking** and use `shift->` or `$_[0]->`: `sub trace { shift->_logger->trace(@_) }`. This is the one place `shift` is right.
- **A builder that ignores `$self` fits on one line:** `sub _build_readonly { 0 }`.
- **`_` prefix marks private** subs and attributes. Builders for private attributes double up: `sub _build__mp`.
### Methods, not bare subs
- **In a class, every helper is a method on `$self`** â not `sub _foo {...}` invoked as `_foo($self->config, $x)`.
- **Per-process caches go on the singleton as an attribute** (`has _cache => ( is => 'ro', default => sub { {} } )`), not a `my %CACHE` package variable.
- **No package-level state** unless it is a true constant (an `%ENGINE_CLASS` lookup table counts; a per-call cache does not).
- Bare subs are fine in **non-class utility modules** imported as functions. Once a file says `use Moose`/`use Moo`, every `sub` is a method.
.claude/skills/getty-perl-core/SKILL.md view on Meta::CPAN
4. **Pin every Getty-authored distribution.** Not stale, not omitted â current.
5. **Re-check on upgrade.**
```bash
cpanm --info Module::Name | tail -1
# â GETTY/Module-Name-1.234.tar.gz â pin to 1.234
```
Getty-authored (non-exhaustive): `Langertha`, `IO::K8s`, `Kubernetes::REST`, `WWW::Crawl4AI`, `Net::Async::Crawl4AI`, `Net::Async::WebSearch`, `Catalyst::Plugin::ChainedURI`, `Locale::Simple`, `DBIO::*`, `WWW::Zitadel`, `WWW::PayPal`, `WWW::Chain`.
## Changelog (the Changes file)
Every distribution ships a `Changes` file with a `{{$NEXT}}` token at the top (Dist::Zilla's `[NextRelease]` fills it at release time).
- **Add a bullet under `{{$NEXT}}` in the SAME commit as any user-facing change** â new bindings, behaviour changes, bug fixes, deprecations. If a CPAN consumer would notice, it belongs there.
- **Match the existing style:** two-space indent, ` - ` bullets, wrap near 78 columns, present-tense imperative ("New binding X", "Fix Y on macOS").
- **One topic, one bullet, one to three lines** â touching an area again rewrites the bullet that is already there instead of adding a second. Wording and length: `getty-git-commit-style`.
- **Skip pure dev-tooling noise** â skill hardlinks, editor config, internal CI refactors. A CI fix that unbreaks the build for everyone IS worth a line.
- **Never hand-edit the version line or timestamp** â `[NextRelease]` owns those.
## Forbidden
.claude/skills/getty-perl-moo/SKILL.md view on Meta::CPAN
## Pattern 7 â Method Modifiers
```perl
before calc => sub { die "x<0" if $_[1] < 0 }; # validate, can't change return
around calc => sub {
my ($orig, $self, $x) = @_;
return $self->$orig($x) + 1; # can change return value
};
after calc => sub { ... }; # side-effects, logging
```
**Rules:** `before`/`after` cannot alter return value; `around` can. Always forward `@_` correctly in `around`. Multiple modifiers from multiple roles stack â order is composition-order-sensitive.
---
## Pattern 8 â Attribute Options Cheatsheet
```perl
has name => (is => 'ro', required => 1);
.claude/skills/getty-perl-release-author-getty/SKILL.md view on Meta::CPAN
### Feature Toggles (Boolean)
- `no_cpan` - Skip UploadToCPAN; also defaults `version_finder` to `:MainModule`
- `no_podweaver` - Skip PodWeaver
- `no_changes` - Skip NextRelease
- `no_installrelease` - Skip InstallRelease
- `no_makemaker` - Skip MakeMaker
- `xs` - Use ModuleBuildTiny (for pure-Perl XS without Alien deps)
- `deprecated` - Add Deprecated plugin
- `adoptme` - Add x_adoptme metadata
- `no_github` - Skip GithubMeta and GitHub::CreateRelease, use Repository instead. Auto-set to 1 when `.git/config` has no github.com remote; set `no_github = 0` to force GitHub plugins on anyway
- `no_github_release` - Skip only GitHub::CreateRelease. Same auto-detection; when active, `dzil release` creates a GitHub Release and attaches the tarball, which needs `~/.github-identity` (login + token)
- `gitea` - Treat the remote host as Gitea/Forgejo (repository/bugtracker/homepage via GiteaMeta). Only needed for self-hosted instances â codeberg.org and the author's own are auto-detected. No effect when a GitHub remote exists
- `include_readme` - Ship README.md (excluded from the tarball by default)
- `no_install` - Resulting distribution can't be installed
- `generate_license` - Go back to a generated LICENSE: `@Basic` keeps its License plugin, no LicenseFile check is added. Default 0 â the bundle expects a committed LICENSE (see above)
### Identity & Metadata
- `author` - CPAN author name used for the authority
- `authority` - Override the authority, e.g. `authority = ETHER` when uploading modules owned by another author (default: the `author` value)
### XS with Alien
.claude/skills/getty-perl-release-author-getty/SKILL.md view on Meta::CPAN
## Release Workflow
```bash
dzil release # Builds, tests, uploads to CPAN, bumps version, commits, tags
```
Read the `{{$NEXT}}` section of `Changes` once as a whole first. It was written bullet
by bullet over weeks, so this is where entries circling one topic get merged into one
and over-detailed ones get cut back (skill `getty-git-commit-style`). It is not only a
changelog: `GitHub::CreateRelease` runs with `notes_from = ChangeLog` and publishes the
section verbatim as the release notes.
Fix it in its own commit *before* `dzil release`. `Changes` sits in the release commit's
`allow_dirty` list, so an uncommitted edit stops nothing â it is folded into the release
snapshot silently, under a message naming only the version.
## Conventions
1. `copyright_year` IS used in dist.ini â GETTY has it in ALL distributions, do NOT remove it
2. No `=head1 SUPPORT/AUTHOR/COPYRIGHT` in POD
.claude/skills/kanban-issues-karr-cli/SKILL.md view on Meta::CPAN
---
# karr â Kanban Assignment & Responsibility Registry
Git-native kanban board for multi-agent workflows. Canonical board state lives in
`refs/karr/*`, not in a checked-in `karr/` directory. Commands materialize a
temporary task/config view only while they run.
`--json` is available on every command with an alternate rendering. `--compact`
is not -- exactly nine render one: `board`, `config`, `context`, `dashboard`,
`list`, `log`, `metrics`, `pick`, `show`. Anywhere else it answers
`Unknown option: compact` with the usage and exit 2, rather than accepting the
flag and ignoring it.
## Referring to a karr id
In prose -- commit subjects, card bodies, anywhere the text travels -- a karr id is
written `k12`, never a bare `#12`: GitHub, Gitea and GitLab all resolve `#12` against
*their own* issue 12, which is a different thing or nothing at all. This is karr's
numbering only and says nothing about how the repository's public issue tracker is
referenced -- that is separate, with its own notation.
.claude/skills/kanban-issues-karr-cli/SKILL.md view on Meta::CPAN
they never fall back to the built-in defaults, which is how a fresh clone used
to answer `board.name: Kanban Board` for a board that has a name. Ask for those
defaults explicitly with `--defaults`: it reads no board (and needs no
repository), so `diff <(karr config show) <(karr config show --defaults)` is
exactly what this board overrides.
### Disable / enable automated agent runs
```bash
karr disable # no automated agent runs here
karr disable --reason "abandoned driver, backlog parked"
karr enable # allow them again
karr disable --json # {"foundation":{"enabled":0,"reason":"â¦"}}
```
Board-level opt-out from `karr-foundation`. Unlike the per-machine `.karr` file
the flag is board state (`foundation.enabled` in `refs/karr/config`), so it syncs
with the board and every foundation instance on every machine honours it. A
disabled board is skipped whole: no drain, no auto-block, no agent run â the
flag wins over `karr-foundation --command`, the config's `default_command`, the
`.karr` `command` and `claude: true`, and `--force` does not override it. Nothing
else changes: the board stays fully usable by hand (`karr list`, `karr pick`,
`karr move`, â¦). Use it for a repository whose backlog is parked rather than
abandoned.
`karr disable` without `--reason` clears any previously stored reason. The same
state is readable and writable through `karr config`:
```bash
karr config get foundation.enabled # -> 0 or 1
karr config set foundation.enabled false # true/false, yes/no, on/off, 1/0
karr config set foundation.reason "why"
```
### Context (board summary for embedding)
```bash
karr context # print markdown summary
karr context --write-to AGENTS.md # create/update file with sentinels
karr context --sections blocked,overdue # filter sections
karr context --days 14 # lookback for recently-completed
karr context --activity-limit 10 # other agents' log entries in Recent Activity
karr context --json # JSON output
karr context --compact # board_name and the four counts, key=value
```
Generates a markdown summary with sections: In Progress, Blocked, Overdue, Recently Completed, Recent Activity (other agents' log entries, newest first, bounded by `--activity-limit`, default 5). `--sections` takes the slugs `in-progress,blocked,over...
### Skill management
```bash
karr skill install # install skill for detected agents
karr skill install --agent claude-code # install for specific agent
karr skill install --global # install globally (~/)
karr skill install --force # force reinstall
karr skill check # check if installed skills are current
karr skill update # update outdated skills
.claude/skills/kanban-issues-karr-cli/SKILL.md view on Meta::CPAN
karr sync
karr sync --pull
karr sync --push
```
Use this when you want explicit control over board ref exchange with the remote
instead of relying only on the implicit pull/push behavior of mutating
commands.
`karr sync` also carries `refs/karr-foundation/*` â karr-foundation's shared
chain, run logs, question mailbox and design documents â in the same run,
after the board and never on its own. One command on purpose: a separate one
would be a second thing to remember, and a coordination namespace nobody
synced fails quietly. Mutating commands still sync the board only, so this
costs nothing outside an explicitly typed `karr sync`, and a repository
holding nothing under `refs/karr-foundation/` pushes nothing there. Deletions
in that namespace (log retention, a cleared chain) travel like board deletions
do, so a pruned run log does not come back on the next pull.
**A fresh clone fetches the board by itself.** `git clone` does not carry
`refs/karr/*`, so a new checkout holds no board while the whole board sits on
its remote. The read commands (`board`, `list`, `show`, `log`, `context`,
`metrics`, `needs`, and `config show`/`config get`) do not pull as a rule â
only mutating commands do â but where there is nothing under `refs/karr/` at
all and the remote has a board, they fetch it once and answer, with one line
on STDERR (never STDOUT) saying where it came from. Where there is no remote,
or the remote has no board, they still refuse with exit 1 rather than
rendering an empty board: that is the only place `karr init` is the answer. In
a clone whose board is on the remote, `karr init` refuses as well and points
at `karr sync`, so it can no longer start a second, empty board beside the
real one; `karr init --new-board` is the documented way through when an
independent board there really is what you want.
.claude/skills/kanban-issues-karr-cli/SKILL.md view on Meta::CPAN
Stores and retrieves helper payloads in Git refs outside protected namespaces
such as `refs/karr/*`, branches, and tags. Use this for shared planning blobs,
agent scratch data, or similar workflow artifacts that should sync through Git
without becoming task cards.
The arguments after the ref are joined with a single space, so they are a
one-line payload. A document goes in on stdin instead â with no content
argument at all, `karr set-refs REF < file` stores the file verbatim and
`karr get-refs REF > file` gives it back unchanged.
### Activity log
```bash
karr log # last 20 entries
karr log --agent swift-fox # filter by agent
karr log --task 5 # filter by task
karr log --last 50 --json # more entries, JSON
karr log --compact # one line per entry, no padding
```
### 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
`created` is still averaged in, negative and all, because every value it could
be clamped to would be an invention. Such samples are counted separately in
`negative_lead_samples` and named in the closing note, so the average is
qualified instead of cleaned â they are *in* the figure, not missing from it,
.claude/skills/kanban-issues-karr-cli/SKILL.md view on Meta::CPAN
NAME=$(karr agent-name) # mint once, reuse everywhere
karr pick --claim "$NAME" --move in-progress
karr handoff ID --claim "$NAME" --note "Implementation complete"
```
Every `karr agent-name` call mints a **new** name and remembers it nowhere, so
`--claim "$(karr agent-name)"` written a second time claims under one name and
hands off under another â while the first claim is live the handoff is refused,
and once it has expired it silently re-stamps the card with a name nobody holds.
Capture the name once into a shell variable and pass that same variable to every
later `--claim`, `--claimed-by` and `log --agent`. If it was never captured, read
it back off the board (`karr show ID` â `Claimed:`, or `karr pick`'s own
`(claimed by NAME)`) rather than minting a fresh one.
## Stored task format
```markdown
id: 1
title: Set up CI pipeline
status: backlog
priority: high
class: standard
created: 2026-03-12T10:00:00Z
updated: 2026-03-12T10:00:00Z
tags:
- devops
- needs:other-repo#7
Optional body with more detail.
```
.claude/skills/kanban-issues-karr-cli/SKILL.md view on Meta::CPAN
materializes the same Markdown shape into a temporary task directory, so this
format still matters when reading or generating tasks programmatically.
## Config refs
```yaml
version: 1
board:
name: My Project
statuses:
- backlog
- todo
- name: in-progress
require_claim: true
- name: review
require_claim: true
- done
- archived
priorities: [low, medium, high, critical]
classes: [expedite, fixed-date, standard, intangible]
claim_timeout: 1h
defaults:
status: backlog
priority: medium
class: standard
foundation:
enabled: false
reason: abandoned driver, backlog parked
```
That YAML lives in `refs/karr/config` as sparse overrides. The next numeric id
is kept separately in `refs/karr/meta/next-id`.
## Decision tree: which command?
1. **Need a board?** â `karr init`
2. **New work item?** â `karr create "Title" --priority high`
3. **What's on the board?** â `karr board` or `karr list`
Revision history for API-Docker
0.004 2026-08-30 03:30:59Z
- The typed object model: `list` and `inspect` on every resource now
return generated `API::Docker::Type::*` objects with snake_case
accessors (`$c->id`, `$c->size_root_fs`) instead of hashrefs. An
unrecognised field is kept and re-sent unchanged rather than dropped,
and `since` is documentation only.
- The hand-written entity classes are gone. Their convenience methods
(`start`, `stop`, `logs`, ...) keep their signatures and move to
`API::Docker::Role::Entity::*`, composed onto the generated classes; the
old names (`API::Docker::Container`, `::Image`, `::Network`, `::Volume`,
`::Plugin`, `::Secret`, `::Config`) ship as stubs that croak, naming what
replaces them.
- New streaming options `on_event`, `on_frame` and `on_chunk` on the HTTP
verbs, wired into `system->events`, `containers->logs`/`stats`/`attach`,
`exec->start`, the `images` build/pull/push/load/get family and the
`plugins` install/upgrade/push. A callback receives each event, frame or
chunk as it arrives; `$stop->()` ends the stream early. Without one the
unbounded endpoints still block.
- New `read_timeout` and `connect_timeout`, as client attributes and
per-request options, off by default. `read_timeout` is idle time since
the last byte; both croak `API::Docker::Error::Timeout`, which carries
whatever already arrived. A new `TIMEOUTS` section in `API::Docker`
documents what each bounds. `containers->stats` is deliberately not
bounded by `read_timeout` -- its stream keeps producing rather than
requires.
- `images->pull` no longer appends a default `tag` onto a reference that
already carries a `:tag` or `@digest`.
- New `images->get`, `->get_all` and `->load`: the image tar roundtrip in
and out of a daemon without a registry. New `images->commit`
(POST /commit) and `images->build_prune` (POST /build/prune, the
BuildKit cache, a different store from the dangling images
`images->prune` deletes).
- New container endpoints: `get_archive`, `put_archive`, `stat_archive`
(the `docker cp` primitives), `changes`, `export`, `resize` and the
one-way half of `attach`. `attach` defaults to `stream => 0, logs => 1`
(replay and return) and refuses a container that is not running unless
`require_running => 0`.
- `containers->start`/`stop`/`restart`/`pause`/`unpause` return 1 when the
call changed the container's state and 0 when it was already in it (the
engine answers a no-op with 304), instead of always undef.
- `containers->stats` croaks `API::Docker::Error::HTTP` when Podman reports
a failure inside a 200 response, instead of handing the error object back
as a reading.
- New `API::Docker::API::Plugins` (`$docker->plugins`): `list`,
`privileges`, `install`, `inspect`, `remove`, `enable`, `disable`,
key carrying CR/LF could open a header line of its own. Not
reachable from this distribution -- the one caller, `push`, passes
the literal `X-Registry-Auth` -- but the option is public. Names are
rejected rather than stripped, unlike values: a value can pick up a
stray newline honestly (`encode_base64` wraps its output by
default), and flattening it keeps what the caller meant, while a
name is a literal the programmer wrote and rewriting
"X-Foo\r\nX-Bar" into "X-FooX-Bar" would put a header on the wire
under a name nobody asked for. The check also catches spaces and
colons, which corrupt the request without injecting anything.
- `containers->logs` and `exec->start` now demultiplex the Docker
stream format and return an ArrayRef of frames, each a HashRef with
`stream` and `data`:
[ { stream => 'stdout', data => "OUT\n" },
{ stream => 'stderr', data => "ERR\n" } ]
Both used to hand the caller the framed bytes, so the 8-byte frame
header of every frame landed inside the log text. Measured against
the rootless Podman socket (5.4.2, API 1.41) with a container
running `echo OUT; echo ERR 1>&2`: without a TTY the body is
`01 00 00 00 00 00 00 04 "OUT\n" 02 00 00 00 00 00 00 04 "ERR\n"`,
and the same exec produces byte-identical output. With a TTY there
is no framing at all -- the body is `"OUT\r\n" "ERR\r\n"` -- which
is why hand-testing interactively never showed the defect. TTY
output comes back as one frame with `stream => 'raw'`, so the shape
never varies and `$_->{stream} eq 'stderr'` is safe on any frame.
Callers wanting plain text use
`join '', map { $_->{data} } @$frames`.
Framing is decided from the response bytes, not from `Content-Type`.
Measured on Podman: `GET /containers/{id}/logs` sends no
`Content-Type` whatsoever, for either kind of container, and
`POST /exec/{id}/start` sends
`application/vnd.docker.raw-stream` for both -- including the
non-TTY exec whose body is in fact multiplexed. Trusting that header
would put frame headers back into the caller's output on that
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
lib/API/Docker/Type/TaskSpec/LogDriver.pm
lib/API/Docker/Type/TaskSpec/NetworkAttachmentSpec.pm
lib/API/Docker/Type/TaskSpec/Placement.pm
lib/API/Docker/Type/TaskSpec/Placement/Preference.pm
lib/API/Docker/Type/TaskSpec/Placement/Preference/Spread.pm
lib/API/Docker/Type/TaskSpec/PluginSpec.pm
lib/API/Docker/Type/TaskSpec/Resources.pm
lib/API/Docker/Type/TaskSpec/RestartPolicy.pm
lib/API/Docker/Type/TaskStatus.pm
lib/API/Docker/Type/ThrottleDevice.pm
lib/API/Docker/Type/Topology.pm
lib/API/Docker/Type/Volume.pm
lib/API/Docker/Type/Volume/UsageData.pm
lib/API/Docker/Type/VolumeCreateOptions.pm
lib/API/Docker/Type/VolumeListResponse.pm
lib/API/Docker/Volume.pm
t/author-pod-syntax.t
t/basic.t
t/connect_timeout.t
t/containers.t
t/containers_endpoints.t
t/containers_stats_error.t
t/dist_source.t
t/distribution.t
t/entity_container.t
t/entity_roles.t
t/exec.t
t/filters.t
t/fixtures/container_inspect.json
t/fixtures/containers_archive.tar
t/fixtures/containers_list.json
t/fixtures/containers_logs_multiplexed.bin
t/fixtures/containers_logs_tty.bin
t/fixtures/containers_logs_tty_json.bin
t/fixtures/exec_start_multiplexed.bin
t/fixtures/images_build_error_stream.ndjson
t/fixtures/images_build_quiet_stream.ndjson
t/fixtures/images_build_stream.ndjson
t/fixtures/images_get.tar
t/fixtures/images_list.json
t/fixtures/images_load_stream.ndjson
t/fixtures/images_pull_stream.ndjson
t/fixtures/networks_list.json
t/fixtures/secrets_list.json
"version" : "0.004"
},
"API::Docker::Type::TaskStatus" : {
"file" : "lib/API/Docker/Type/TaskStatus.pm",
"version" : "0.004"
},
"API::Docker::Type::ThrottleDevice" : {
"file" : "lib/API/Docker/Type/ThrottleDevice.pm",
"version" : "0.004"
},
"API::Docker::Type::Topology" : {
"file" : "lib/API/Docker/Type/Topology.pm",
"version" : "0.004"
},
"API::Docker::Type::Volume" : {
"file" : "lib/API/Docker/Type/Volume.pm",
"version" : "0.004"
},
"API::Docker::Type::Volume::UsageData" : {
"file" : "lib/API/Docker/Type/Volume/UsageData.pm",
"version" : "0.004"
},
"Dist::Zilla::Plugin::Git::Commit" : {
"add_files_in" : [],
"commit_msg" : "v%V%n%n%c",
"signoff" : 0
},
"Dist::Zilla::Role::Git::DirtyFiles" : {
"allow_dirty" : [
"Changes"
],
"allow_dirty_match" : [],
"changelog" : "Changes"
},
"Dist::Zilla::Role::Git::Repo" : {
"git_version" : "2.47.3",
"repo_root" : "."
},
"Dist::Zilla::Role::Git::StringFormatter" : {
"time_zone" : "local"
}
},
"name" : "@Author::GETTY/@Git::VersionManager/release snapshot",
"version" : "2.052"
},
{
"class" : "Dist::Zilla::Plugin::Git::Tag",
"config" : {
"Dist::Zilla::Plugin::Git::Tag" : {
"branch" : null,
"changelog" : "Changes",
"signed" : 0,
"tag" : "0.004",
"tag_format" : "%v",
"tag_message" : "v%V"
},
"Dist::Zilla::Role::Git::Repo" : {
"git_version" : "2.47.3",
"repo_root" : "."
},
"Dist::Zilla::Role::Git::StringFormatter" : {
"Dist::Zilla::Role::Git::DirtyFiles" : {
"allow_dirty" : [
"Build.PL",
"Changes",
"Makefile.PL"
],
"allow_dirty_match" : [
"(?^:^bin/)",
"(?^:^lib/.*\\.pm$)"
],
"changelog" : "Changes"
},
"Dist::Zilla::Role::Git::Repo" : {
"git_version" : "2.47.3",
"repo_root" : "."
},
"Dist::Zilla::Role::Git::StringFormatter" : {
"time_zone" : "local"
}
},
"name" : "@Author::GETTY/@Git::VersionManager/post-release commit",
version: '0.004'
API::Docker::Type::TaskSpec::RestartPolicy:
file: lib/API/Docker/Type/TaskSpec/RestartPolicy.pm
version: '0.004'
API::Docker::Type::TaskStatus:
file: lib/API/Docker/Type/TaskStatus.pm
version: '0.004'
API::Docker::Type::ThrottleDevice:
file: lib/API/Docker/Type/ThrottleDevice.pm
version: '0.004'
API::Docker::Type::Topology:
file: lib/API/Docker/Type/Topology.pm
version: '0.004'
API::Docker::Type::Volume:
file: lib/API/Docker/Type/Volume.pm
version: '0.004'
API::Docker::Type::Volume::UsageData:
file: lib/API/Docker/Type/Volume/UsageData.pm
version: '0.004'
API::Docker::Type::VolumeCreateOptions:
file: lib/API/Docker/Type/VolumeCreateOptions.pm
version: '0.004'
class: Dist::Zilla::Plugin::Git::Commit
config:
Dist::Zilla::Plugin::Git::Commit:
add_files_in: []
commit_msg: v%V%n%n%c
signoff: 0
Dist::Zilla::Role::Git::DirtyFiles:
allow_dirty:
- Changes
allow_dirty_match: []
changelog: Changes
Dist::Zilla::Role::Git::Repo:
git_version: 2.47.3
repo_root: .
Dist::Zilla::Role::Git::StringFormatter:
time_zone: local
name: '@Author::GETTY/@Git::VersionManager/release snapshot'
version: '2.052'
-
class: Dist::Zilla::Plugin::Git::Tag
config:
Dist::Zilla::Plugin::Git::Tag:
branch: ~
changelog: Changes
signed: 0
tag: '0.004'
tag_format: '%v'
tag_message: v%V
Dist::Zilla::Role::Git::Repo:
git_version: 2.47.3
repo_root: .
Dist::Zilla::Role::Git::StringFormatter:
time_zone: local
name: '@Author::GETTY/@Git::VersionManager/Git::Tag'
commit_msg: 'increment $VERSION after %v release'
signoff: 0
Dist::Zilla::Role::Git::DirtyFiles:
allow_dirty:
- Build.PL
- Changes
- Makefile.PL
allow_dirty_match:
- (?^:^bin/)
- (?^:^lib/.*\.pm$)
changelog: Changes
Dist::Zilla::Role::Git::Repo:
git_version: 2.47.3
repo_root: .
Dist::Zilla::Role::Git::StringFormatter:
time_zone: local
name: '@Author::GETTY/@Git::VersionManager/post-release commit'
version: '2.052'
-
class: Dist::Zilla::Plugin::Git::Push
config:
lib/API/Docker.pm view on Meta::CPAN
package API::Docker;
# ABSTRACT: Perl client for the Docker Engine API
our $VERSION = '0.004';
use Moo;
use Carp qw( croak );
use Log::Any qw( $log );
use API::Docker::API::System;
use API::Docker::API::Containers;
use API::Docker::API::Images;
use API::Docker::API::Networks;
use API::Docker::API::Volumes;
use API::Docker::API::Exec;
use API::Docker::API::Distribution;
use API::Docker::API::Secrets;
use API::Docker::API::Configs;
lib/API/Docker.pm view on Meta::CPAN
is => 'lazy',
builder => sub { API::Docker::API::Plugins->new(client => $_[0]) },
);
sub negotiate_version {
my ($self, %opts) = @_;
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
lib/API/Docker.pm view on Meta::CPAN
croak __PACKAGE__ . '->negotiate_version: GET /version must answer with a '
. 'JSON object carrying an ApiVersion of the form N.N (e.g. "1.44"); got '
. $got
unless ref $version_info eq 'HASH'
&& defined $version_info->{ApiVersion}
&& !ref $version_info->{ApiVersion}
&& $version_info->{ApiVersion} =~ /^\d+\.\d+$/;
$self->_set_api_version($version_info->{ApiVersion});
$log->debugf("Negotiated API version: %s", $version_info->{ApiVersion});
$self->_version_negotiated(1);
}
around _request => sub {
my ($orig, $self, $method, $path, %opts) = @_;
# Auto-negotiate before any versioned request, but not for /version itself.
# The triggering request's own bounds are handed to it: the negotiation is a
# pre-flight the caller never wrote, and a caller who asked for a bound and
lib/API/Docker.pm view on Meta::CPAN
=item * Unix socket and TCP transport, the latter in the clear or over TLS
with client certificates (L</tls>, L</cert_path>)
=item * Automatic API version negotiation
=item * A typed object model generated from Docker's own swagger
(L<API::Docker::Type>) -- complete across all seven resources: C<list> and
C<inspect> return these generated classes, not hashrefs; see
L</Architecture> below
=item * Comprehensive logging via L<Log::Any>
=back
=head2 Architecture
The distribution is organized into several layers:
=over
=item * B<Main Client> - L<API::Docker> - Entry point with API version negotiation
lib/API/Docker.pm view on Meta::CPAN
C<< $docker->containers->using(read_timeout => 5)->list >> on a fresh client
bounds the C<GET /version> as well as the list. It is the one place the two
options are still written out per call, because it can be called directly and
is not reached through a resource class:
$docker->negotiate_version(read_timeout => 5);
=back
The entity classes have no C<using> of their own; a bound for
C<< $container->logs >> goes on the resource class instead, see
L<API::Docker::Role::Using/"What has no clone of its own">.
=head1 CONTAINER ENGINES
This client speaks the Docker Engine HTTP API over a socket. It never shells
out to the C<docker> binary, so any engine serving that API works, whether or
not Docker itself is installed.
=head2 Installing Docker
lib/API/Docker/API/Containers.pm view on Meta::CPAN
$params{v} = $opts{volumes} ? 1 : 0 if defined $opts{volumes};
$params{force} = $opts{force} ? 1 : 0 if defined $opts{force};
$params{link} = $opts{link} ? 1 : 0 if defined $opts{link};
return $self->client->delete_request("/containers/$id",
params => \%params,
%{ $self->_request_options },
);
}
sub logs {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
my %params;
$params{follow} = $opts{follow} ? 1 : 0 if defined $opts{follow};
$params{stdout} = defined $opts{stdout} ? ($opts{stdout} ? 1 : 0) : 1;
$params{stderr} = defined $opts{stderr} ? ($opts{stderr} ? 1 : 0) : 1;
$params{since} = $opts{since} if defined $opts{since};
$params{until} = $opts{until} if defined $opts{until};
$params{timestamps} = $opts{timestamps} ? 1 : 0 if defined $opts{timestamps};
$params{tail} = $opts{tail} if defined $opts{tail};
# exists, not truth: an unset callback is a caller bug, and quietly falling
# back to the buffered path for it would answer a follow with a hang.
return $self->client->stream_frames('GET', "/containers/$id/logs",
params => \%params,
defined $opts{tty} ? ( tty => $opts{tty} ) : (),
%{ $self->_request_options },
exists $opts{on_frame} ? ( on_frame => $opts{on_frame} ) : (),
);
}
# The guard behind attach's require_running, and a pre-flight check is all it
# is: it asks the engine what the container is doing now, and the container may
lib/API/Docker/API/Containers.pm view on Meta::CPAN
my $state = $inspected->state;
return unless blessed($state) && defined $state->running;
return if $state->running;
my $status = $state->status;
$status = 'not running' unless defined $status && length $status;
croak __PACKAGE__ . '->attach refused: container ' . $id . ' is ' . $status
. '. Attaching to a container that is not running destroys its exit status '
. 'on Podman, irrecoverably -- the engine keeps no copy -- and with '
. 'stream => 1 never returns on either engine. Read its output with logs() '
. 'instead, or pass require_running => 0 to attach anyway';
}
sub attach {
my ($self, $id, %opts) = @_;
croak "Container ID required" unless $id;
# Pre-flight, and deliberately before the request is built: the call itself
# is what destroys the exit status, so a check made afterwards could only
# report the loss rather than prevent it. Turned off it costs nothing at all,
lib/API/Docker/API/Containers.pm view on Meta::CPAN
my $require_running
= defined $opts{require_running} ? $opts{require_running} : 1;
# The pre-flight is a request the caller never wrote, and one that hangs is
# exactly what a bound was set to prevent -- so it carries the same one. It
# does that by itself here: the check runs on $self, which is the clone
# ->using returned when there was one (karr k74).
$self->_assert_container_running($id) if $require_running;
my %params;
$params{stream} = $opts{stream} ? 1 : 0;
$params{logs} = defined $opts{logs} ? ($opts{logs} ? 1 : 0) : 1;
$params{stdout} = defined $opts{stdout} ? ($opts{stdout} ? 1 : 0) : 1;
$params{stderr} = defined $opts{stderr} ? ($opts{stderr} ? 1 : 0) : 1;
$params{stdin} = $opts{stdin} ? 1 : 0 if defined $opts{stdin};
return $self->client->stream_frames('POST', "/containers/$id/attach",
params => \%params,
defined $opts{tty} ? ( tty => $opts{tty} ) : (),
%{ $self->_request_options },
exists $opts{on_frame} ? ( on_frame => $opts{on_frame} ) : (),
);
}
lib/API/Docker/API/Containers.pm view on Meta::CPAN
$docker->containers->start($result->{Id});
# Inspect container details
my $container = $docker->containers->inspect($result->{Id});
say $container->name;
# Stop and remove
$docker->containers->stop($result->{Id}, timeout => 10);
$docker->containers->remove($result->{Id});
# View logs (ArrayRef of { stream => 'stdout'|'stderr'|'raw', data => ... })
my $frames = $docker->containers->logs($result->{Id}, tail => 100);
my $text = join '', map { $_->{data} } @$frames;
# Attach one-way: replays the same frames and returns (stream => 0 by
# default -- stream => 1 on a stopped container never returns). On Podman,
# attaching to a container that has ALREADY EXITED destroys its exit
# status; use logs() for that case, see attach()
my $attached = $docker->containers->attach($result->{Id});
# Copy a file out, and a tar archive in (what docker cp is built on)
my $tar = $docker->containers->get_archive($result->{Id},
path => '/etc/hostname');
$docker->containers->put_archive($result->{Id}, $tar, path => '/tmp');
=head1 DESCRIPTION
This module provides methods for managing Docker containers including creation,
lifecycle operations (start, stop, restart), inspection, logs, and more.
C<list> and C<inspect> return generated L<API::Docker::Type> objects carrying
the convenience methods of L<API::Docker::Role::Entity::Container>, so
C<< $container->start >> and C<< $container->logs >> work on either. Which
class each returns, and where the two disagree, is below.
Accessed via C<< $docker->containers >>, or through
L<API::Docker::Role::Using/using> for a run of calls that needs its own
transport bound: C<< $docker->containers->using(read_timeout => 5) >>.
=head2 The two container shapes
The daemon describes a container two ways and the swagger has two
definitions for it, so this class returns two classes:
lib/API/Docker/API/Containers.pm view on Meta::CPAN
no-op (304, where the engine sends one). C<kill> has only one, because
C<_request> croaks on any C<< status >= 400 >>, so the B<409> a non-running
container gets back never reaches this method's C<return>. A boolean with a
single possible value is not worth adding.
More importantly, B<204 does not mean the container stopped.> Measured on
B<both> engines -- Docker 29.7.2 (API 1.55) and rootless Podman 5.4.2 (API
1.41), same machine, identical behavior: sending a signal the container traps
or ignores -- C<< signal => 'SIGUSR1' >> against a process with a handler
installed for it -- is delivered, the container keeps running, the handler's
output turns up in L</logs>, and the engine still answers 204 exactly as it
does for a signal that does end the process. A caller that needs to know
whether the container is still running after a C<kill> has to ask
L</inspect>; that is also why this returns nothing rather than a plain C<1>
-- a 1 here would claim a state change that a trapped signal never made.
B<A paused container is where the two engines part.> Both answer 204 to
C<< signal => 'SIGUSR1' >> against a paused container, and then:
=over
lib/API/Docker/API/Containers.pm view on Meta::CPAN
=over
=item * C<force> - Force removal (kill if running)
=item * C<volumes> - Remove associated volumes
=item * C<link> - Remove specified link
=back
=head2 logs
my $frames = $containers->logs($id, tail => 100, timestamps => 1);
# stdout and stderr, in the order the engine emitted them
my $text = join '', map { $_->{data} } @$frames;
# stderr only
my @errors = grep { $_->{stream} eq 'stderr' } @$frames;
Get container logs. Returns an ArrayRef of frames, each a HashRef with
C<stream> and C<data>:
[ { stream => 'stdout', data => "OUT\n" },
{ stream => 'stderr', data => "ERR\n" } ]
A container created without a TTY multiplexes stdout and stderr into a single
framed stream, and this method demultiplexes it -- without that, the 8-byte
frame headers end up in the caller's log text. A container created B<with> a
TTY writes to one pty and the engine sends no frame headers, so its whole
output arrives as a single frame with C<< stream => 'raw' >>: with a TTY there
is no stdout/stderr distinction left to report. C<stream> is always a plain
string, so C<< $_->{stream} eq 'stderr' >> is safe on any frame.
Framing is detected from the response bytes, because the engine's
C<Content-Type> cannot be trusted for it -- see
L<API::Docker::Role::HTTP/"Detecting a framed stream"> for the rule and its one
failure mode.
lib/API/Docker/API/Containers.pm view on Meta::CPAN
=over
=item * C<follow> - Keep the connection open and send new output as the
container writes it. Only usable with C<on_frame>; see below
=item * C<stdout> - Include stdout (default 1)
=item * C<stderr> - Include stderr (default 1)
=item * C<since> - Show logs since timestamp
=item * C<until> - Show logs before timestamp
=item * C<timestamps> - Include timestamps
=item * C<tail> - Number of lines from end (e.g., C<100> or C<all>)
=item * C<tty> - Set to 1 when the container was created with a TTY and its
output is binary, to skip demultiplexing. Not needed for text output. The
container's own setting is C<Config.Tty> from C<< $containers->inspect($id) >>.
With C<on_frame> it is a declaration rather than a hint; see below
=item * C<on_frame> - CodeRef called with each frame as it arrives, instead of
the ArrayRef being collected and returned; see below
=back
=head2 Following the log
C<< follow => 1 >> asks the daemon to keep sending as the container writes.
Pass C<on_frame> with it and the frames are handed over as they arrive:
my $summary = $containers->logs($id,
follow => 1,
tail => 0,
on_frame => sub {
my ($frame, $stop) = @_;
print $frame->{data};
$stop->() if $frame->{data} =~ /listening on/;
},
);
$summary; # { delivered => 4, stopped => 1 }
With a callback the return value is that summary HashRef, not the frames:
C<delivered> is how many went to the callback, C<stopped> is 1 when the
callback ended the stream and 0 when the daemon did. Nothing is accumulated --
a followed log is unbounded by construction, and the callback has been handed
every frame already. See
L<API::Docker::Role::HTTP/"Streaming a response as it arrives">.
B<Without a callback, C<< follow => 1 >> blocks> until the container exits or
the daemon closes the connection, because the whole response is read before
anything is parsed. Use it with C<on_frame> or not at all.
C<tty> means something stronger on this path. The buffered path decides
framing by walking the whole body (see
L<API::Docker::Role::HTTP/"Detecting a framed stream">), which is exactly what
lib/API/Docker/API/Containers.pm view on Meta::CPAN
either way -- a TTY stream arrives as a series of C<< stream => 'raw' >>
frames rather than the single one the buffered path builds.
=head2 attach
my $frames = $containers->attach($id);
my $text = join '', map { $_->{data} } @$frames;
Attach to a container's streams and return everything they produced, as an
ArrayRef of frames in the same shape L</logs> returns:
[ { stream => 'stdout', data => "OUT\n" },
{ stream => 'stderr', data => "ERR\n" } ]
A container created without a TTY multiplexes its output into one framed
stream, which this method demultiplexes; one created with a TTY arrives as a
single C<< stream => 'raw' >> frame. See L</logs> and
L<API::Docker::Role::HTTP/"Detecting a framed stream">.
B<The container must be running.> Attaching to one that has already exited
destroys its exit status on Podman, so this method checks first and croaks
rather than attaching -- read the output of a finished container with
L</logs>. Both halves of that are worth knowing before the call: see
L</"On Podman this destroys a stopped container's exit status"> and
L</"This method refuses a container that is not running">.
=head2 On Podman this destroys a stopped container's exit status
B<Attaching to a container that has already exited loses its exit code on
Podman, and nothing reports it.> Measured before and after a single attach
against one container that exited with 4, on Podman 5.4.2 (API 1.41) and
Docker 29.7.2 (API 1.55), same machine:
PODMAN before inspect: exited 4 wait: { StatusCode => 4 }
PODMAN after inspect: created 0 wait: { StatusCode => -1 }
DOCKER before inspect: exited 4 wait: { StatusCode => 4 }
DOCKER after inspect: exited 4 wait: { StatusCode => 4 }
Podman reverts the container to C<created>, resets C<ExitCode> to 0, and
answers a later L</wait> with the sentinel C<-1> inside a 200. The real value
is gone from the engine; there is nothing to read it back from. B<All three
variants do it> -- C<< stream => 1 >> with and without C<logs>, and the
C<< stream => 0, logs => 1 >> this method now sends by default, which is the
one that returns cleanly in milliseconds. It is the call that does it, not
the hang.
L</logs> does B<not> do it, and Docker does not do it at all. So on Podman
the sequence "attach to collect the output, then L</wait> for the exit code"
cannot work: read the output with L</logs> instead, or take the exit code
before attaching.
=head2 This method refuses a container that is not running
Because of the above, C<attach> asks L</inspect> whether the container is
running and B<croaks instead of attaching> when it is not:
API::Docker::API::Containers->attach refused: container x is exited. ...
C<< require_running => 0 >> turns that off and attaches anyway; the check is
lib/API/Docker/API/Containers.pm view on Meta::CPAN
A container that is still running when the attach is sent therefore stays
safe even when it exits a millisecond later. The damage needs the container to
be stopped B<already>, which is the common case and the one the check catches:
a caller reaching for a container it knows has finished.
Refusing costs a caller nothing that C<attach> could have given them, on
either engine. Against a container that is not running there is no combination
that is both safe and useful: on Podman every variant destroys the exit
status, C<< stream => 1 >> hangs forever on B<both> engines (measured: Docker
was still open when a 10 s probe gave up), and Docker's C<< stream => 0 >>
replay returns the same frames L</logs> returns, with none of the hazard and
with C<tail> and C<since> on top.
The check is engine-independent although the data loss is Podman's alone.
Telling the engines apart would cost a round trip of its own, it would leave
the both-engine C<< stream => 1 >> hang in place, and it would make the safe
call on Docker a call this distribution recommends against anywhere else.
B<This is a behavior change.> Up to and including the previous release the
call went straight to the engine.
=head2 This is the one-way attach
The engine has two attach protocols behind one path. Sent with
C<Upgrade: tcp> and C<Connection: Upgrade>, C<< POST /containers/{id}/attach >>
answers B<101 Switching Protocols> and hands over a bidirectional connection:
that is what C<docker attach> uses, and it is what lets a caller type into the
container's stdin. Sent B<without> those headers -- which is what this method
does -- the engine answers B<200> and streams the container's output one way,
in exactly the frames L</logs> returns.
This method implements the second one only, because the transport here buffers
a whole response before returning it (see
L<API::Docker::Role::HTTP/"What the transport does not do">). Two consequences
a caller has to plan around:
=over
=item * B<You cannot write to the container.> C<< stdin => 1 >> is passed to
the engine, but this client sends no bytes after the request headers and then
reads until the daemon closes, so there is no moment at which input could be
supplied. Use L<API::Docker::API::Exec> to run something interactive-shaped,
or wait for the upgraded variant
=item * B<Without a callback it returns when the stream ends, not before.>
With C<< stream => 1 >>, attaching to a container that keeps running blocks
until it exits or the daemon closes the connection -- and on a container that
is B<not> running it never returns at all, see
L</"The defaults follow the engine"> below. Pass C<on_frame> to read the
stream as it arrives and stop where you like, exactly as L</logs> does under
L</"Following the log">; the return value is then the summary HashRef
C<< { delivered => N, stopped => 0|1 } >> rather than the frames, and C<tty>
becomes a declaration the transport takes at its word -- an undeclared
unframed stream croaks. For a running container that need not be attached to,
L</logs> with C<tail> reads the same output and returns immediately
=back
C</containers/{id}/attach/ws>, the WebSocket variant, is not implemented
either.
=head2 The defaults follow the engine
C<stream> defaults to B<0> -- the engine's own default -- and C<logs> to
B<1>, which is the one flag that keeps the call useful without it. So
C<< $containers->attach($id) >> B<replays> what the container has written and
returns.
B<This is a change.> Up to and including the previous release C<stream>
defaulted to 1, so the same call opened an open-ended subscription; a caller
who wants the live stream now has to ask for it with C<< stream => 1 >>.
The reason is that the subscription has exactly one terminator: the container
ending. C<stream> means I<stream attached streams from the time the request
lib/API/Docker/API/Containers.pm view on Meta::CPAN
connection -- the response carries no C<Content-Length> and no chunked
terminator -- so HTTP framing cannot signal the end either. The transport
reads until EOF, there is no EOF, and the call hangs. C<on_frame> does not
help: nothing will ever call C<< $stop->() >>.
Measured on Podman 5.4.2 (API 1.41), all four against one and the same
container:
=over
=item * C<?logs=1&stdout=1&stderr=1&stream=0>, exited container -- 200, the
frames, connection closed after 13 ms
=item * C<?logs=1&stdout=1&stderr=1&stream=1>, exited container -- 200, the
same frames, then hangs
=item * the same with C<Upgrade: tcp> -- 101 UPGRADED, the same frames, still
hangs
=item * C<?stream=1> while the container is still B<running> and exits three
seconds later -- closes cleanly after 3 s
=back
B<Docker does exactly the same, and that is measured now too.> Against Docker
29.7.2 (API 1.55): C<?logs=1&stdout=1&stderr=1&stream=1> on an exited
container was still open when a 10 s probe gave up, and
C<?logs=1&stdout=1&stderr=1&stream=0> answered 200 with byte-identical frames
and closed in half a millisecond. So the hang is not a Podman quirk to be
worked around -- it is what both engines do with a subscription whose only
terminator is already in the past, on an endpoint whose reference promises a
close in neither direction. It is unspecified behavior on both, which is the
case for the C<< stream => 0 >> default rather than an argument against it.
One more measured difference: Podman refuses C<< stream => 0 >> together with
C<< logs => 0 >> outright, with B<400> C<at least one of Logs or Stream must
be set>, rather than answering an empty 200.
Options:
=over
=item * C<stream> - Subscribe to what the container writes from the time of
the request onwards. Default B<0>, which is the engine's own default.
C<< stream => 1 >> on a container that is not running never returns; see
L</"The defaults follow the engine">
=item * C<logs> - Replay what the container has already written. Default
B<1>, so the call returns something without subscribing; combined with
C<< stream => 1 >> the replay comes first and then transitions seamlessly
into the live output. C<< logs => 0 >> without C<< stream => 1 >> is the
combination the engine refuses (400 on Podman)
=item * C<stdout> - Attach stdout. Default 1 (engine default: false)
=item * C<stderr> - Attach stderr. Default 1 (engine default: false)
=item * C<stdin> - Attach stdin. Sent as asked, but nothing can be written to
it here; see above
=item * C<tty> - Set to 1 when the container was created with a TTY and its
output is binary, to skip demultiplexing. Same meaning as in L</logs>, and
with C<on_frame> the same promise
=item * C<on_frame> - CodeRef called with each frame as it arrives, instead of
the ArrayRef being collected and returned. Same contract as in L</logs>
=item * C<require_running> - Ask L</inspect> whether the container is running
first, and croak rather than attach when it is not. Default B<1>. Set to 0 to
attach to a stopped container anyway, which also skips the round trip; see
L</"This method refuses a container that is not running"> for what the check
does and does not guarantee
=back
=head2 top
lib/API/Docker/API/Containers.pm view on Meta::CPAN
is parsed. It then returns an ArrayRef of readings rather than the single
HashRef the one-shot call returns, which is the other reason to pass a
callback instead.
Unlike L<API::Docker::API::System/events>, this does not turn the stream's
error check off. C</events> is a feed of engine records, where an
C<errorDetail> object would still be data; a stats stream is one container's
readings, and the transport's default is to croak on a failure reported inside
a 200 body (L<API::Docker::Role::HTTP/"Failure inside a 200 response">).
The default is kept on a measurement rather than on that analogy. Against
Podman 5.4.2 (API 1.41) every object a running container's stream carries is
a complete reading -- C<read>, C<cpu_stats>, C<memory_stats>, C<networks> and
the rest -- and killing the container and then removing it while the stream
was open ended the stream on a whole reading, with nothing appended after it.
No C<errorDetail> was sent in either case, and the Engine API reference names
that key for C</build>, C</images/create> and C</images/{name}/push> alone.
So the check has no legitimate reading here it could turn into a croak, and
that -- not an unexamined default -- is why it stays on.
Options:
lib/API/Docker/API/Containers.pm view on Meta::CPAN
the engine's body type-check requires.
=head2 get_archive
use Path::Tiny;
my $tar = $containers->get_archive($id, path => '/etc/hostname');
path('hostname.tar')->spew_raw($tar);
# and what the path was, without a second request
my %stat;
my $tar = $containers->get_archive($id, path => '/var/log', stat => \%stat);
say $stat{name};
Read a path out of a container as a tar archive -- the outbound half of
C<docker cp>. Returns the raw archive bytes, never decoded and never modified.
A file comes back as a one-member archive named after its basename; a
directory comes back as the directory and everything under it, with paths
relative to its parent. The whole archive is buffered in memory.
Options:
lib/API/Docker/API/Exec.pm view on Meta::CPAN
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
L<API::Docker::API::Containers/logs>:
[ { stream => 'stdout', data => "OUT\n" },
{ stream => 'stderr', data => "ERR\n" } ]
An exec instance created without a TTY multiplexes stdout and stderr into one
framed stream, which this method demultiplexes. One created with a TTY has no
frame headers and its output arrives as a single C<< stream => 'raw' >> frame.
A detached start produces no output, so it returns an empty ArrayRef.
The exit status is B<not> part of this response. It comes from a separate call
lib/API/Docker/API/System.pm view on Meta::CPAN
# Monitor events
my $events = $docker->system->events(
since => time() - 3600,
);
# Disk usage
my $df = $docker->system->df;
# Check registry credentials before doing the work that needs them
my $login = $docker->system->auth(
username => 'me',
password => 'secret',
serveraddress => 'ghcr.io',
);
say $login->{Status}; # Login Succeeded
=head1 DESCRIPTION
This module provides access to Docker system-level operations including daemon
information, version detection, health checks, and event monitoring.
Accessed via C<< $docker->system >>, or through
L<API::Docker::Role::Using/using> for a run of calls that needs its own
transport bound: C<< $docker->system->using(read_timeout => 5) >>.
lib/API/Docker/API/System.pm view on Meta::CPAN
=head2 df
my $usage = $system->df;
Get data usage information (disk usage by images, containers, and volumes).
Returns hashref with C<LayersSize>, C<Images>, C<Containers>, and C<Volumes> arrays.
=head2 auth
my $login = $system->auth(
username => 'me',
password => 'secret',
serveraddress => 'ghcr.io',
);
# Or hand over the same auth argument images->push takes
$system->auth(auth => $auth);
Check a set of registry credentials against the registry, without pulling or
pushing anything. Returns the decoded C<< POST /auth >> response, a HashRef
lib/API/Docker/API/System.pm view on Meta::CPAN
=back
Passing neither C<auth> nor any credential key croaks before the request is
made.
=head3 What Podman answers
Measured against the rootless Podman socket (5.4.2, API 1.41): the endpoint
exists, but a failed check is B<500 Internal Server Error>, not Docker's 401,
and the message is the registry's own text wrapped by Podman --
C<< {"message":"login attempt to 127.0.0.1:1 failed with status: ..."} >>.
An empty AuthConfig answers
C<< {"message":"login attempt to failed with status: getting username and
password: cannot prompt for username without stdin"} >>, also 500. So the
croak is reliable on both engines while the status behind it is not: test the
outcome, not the number.
=head1 SEE ALSO
=over
=item * L<API::Docker> - Main Docker client
lib/API/Docker/Container.pm view on Meta::CPAN
# anything, and xt/release/changes_has_content.t, which only reads Changes.
my $REFUSED =
__PACKAGE__ . ' was removed in API::Docker 0.004 and this file is a stub'
. ' with nothing in it: it ships only so that installing this'
. ' release overwrites the working copy an earlier one left on'
. ' disk. You have not hit a fault in the distribution. The'
. ' containers the daemon answers with are'
. ' API::Docker::Type::ContainerSummary (containers->list) and'
. ' API::Docker::Type::ContainerInspectResponse'
. ' (containers->inspect), with the field names the swagger\'s'
. ' own in snake_case, and start, stop, logs, is_running and the'
. ' rest are unchanged on them, composed in from'
. ' API::Docker::Role::Entity::Container. This stub refuses';
# The croak below is what a caller normally hits. AUTOLOAD is for the one who
# swallowed it -- eval { require API::Docker::Container } and then called a
# method anyway; the answer has to be the same one, not a bare "Can't locate
# object method". DESTROY is defined so it does not reach AUTOLOAD.
sub AUTOLOAD { croak $REFUSED }
sub DESTROY { }
lib/API/Docker/Container.pm view on Meta::CPAN
the file is still in the distribution.
What to reach for instead:
=over
=item * L<API::Docker::Type::ContainerSummary> -- what C<< containers->list >> returns
=item * L<API::Docker::Type::ContainerInspectResponse> -- what C<< containers->inspect >> returns
=item * L<API::Docker::Role::Entity::Container> -- start, stop, logs, is_running and the rest,
unchanged, composed into the above at load time
=back
Where this class mirrored the daemon's CamelCase verbatim, the generated
classes carry the swagger's own names in snake_case.
L<API::Docker::API::Containers> documents the shape each method returns.
=head1 SEE ALSO
lib/API/Docker/Error/Truncated.pm view on Meta::CPAN
It is a structural check, not a heuristic, and it asks one of two questions
depending on how the piece is delimited. Where the response announced a length
it compares what arrived against it. Where the framing is by terminator
instead -- the head, and the chunk headers -- it asks whether the terminator
came before the stream ended, which needs nothing to compare and is just as
decidable. Neither is a guess about content: a header block that never closed
is not a short one, it is an unfinished one.
A body delimited by nothing but the close -- C<attach>,
C<< logs(follow => 1) >>, C</exec/{id}/start>, the whole
C<application/vnd.docker.raw-stream> family -- announces no end and has no
terminator either, so there an EOF B<is> the end and this is never raised.
Its B<head> is framed like any other, and is checked like any other.
=head2 Why it is fatal
For the same reason L<API::Docker::Error::Timeout> is, and the two are the
same defect reached by different routes: a short body satisfies every return
shape this role promises and is indistinguishable from a complete one.
C<ndjson> promises an ArrayRef of events and gets a shorter one; C<raw>
lib/API/Docker/Role/Entity.pm view on Meta::CPAN
}
# at the bottom of the same file: the methods land on the generated class
Moo::Role->apply_roles_to_package(
'API::Docker::Type::ContainerSummary', __PACKAGE__);
=head1 DESCRIPTION
An entity is a generated L<API::Docker::Type> class that has been given the
convenience methods of its resource -- C<< $container->start >>,
C<< $container->logs >>, C<< $image->remove >>. The methods live in a role
that is applied to the generated class at load time; they are never written
into the generated file.
=head2 Why the methods are not in the generated class
They cannot be. C<maint/spec-to-type.pl --verify> renders every class under
C<lib/API/Docker/Type/> out of C<spec/v1.51.yaml> and requires the result to
match what is shipped B<byte for byte> (F<t/spec_to_type.t>), and the
generator refuses to overwrite a file that exists. A hand-added C<with> line
or method in one of those files fails the suite; there is no mode of the
lib/API/Docker/Role/Entity/Container.pm view on Meta::CPAN
return $self->client->containers->kill($self->id, %opts);
}
sub remove {
my ($self, %opts) = @_;
return $self->client->containers->remove($self->id, %opts);
}
sub logs {
my ($self, %opts) = @_;
return $self->client->containers->logs($self->id, %opts);
}
sub attach {
my ($self, %opts) = @_;
return $self->client->containers->attach($self->id, %opts);
}
sub inspect {
lib/API/Docker/Role/Entity/Container.pm view on Meta::CPAN
# from list: an API::Docker::Type::ContainerSummary
my ($container) = @{ $docker->containers->list };
say $container->id;
say $container->status; # "Up 2 hours"
say $container->state; # "running"
$container->start;
$container->stop(timeout => 10);
my $logs = $container->logs(tail => 100);
$container->remove(force => 1);
# from inspect: an API::Docker::Type::ContainerInspectResponse, where
# the same methods work and `state` is an object
my $full = $docker->containers->inspect($container->id);
say $full->state->status;
say $full->state->exit_code;
if ($full->is_running) { ... }
lib/API/Docker/Role/Entity/Container.pm view on Meta::CPAN
$container->kill(signal => 'SIGTERM');
Send a signal to the container.
=head2 remove
$container->remove(force => 1);
Remove the container.
=head2 logs
my $logs = $container->logs(tail => 100);
# or follow it, one frame at a time
$container->logs(follow => 1, tail => 0,
on_frame => sub { print $_[0]{data} });
Get container logs. Every option goes to
L<API::Docker::API::Containers/logs>, C<follow> and C<on_frame> included; with
a callback the return value is that method's summary HashRef rather than the
frames.
=head2 attach
my $frames = $container->attach;
Attach to the container's output and return the frames, one-way. Every option
goes to L<API::Docker::API::Containers/attach>, C<on_frame> included; with a
callback the return value is that method's summary HashRef rather than the
frames. Without options it replays what the container already wrote and
returns; C<< stream => 1 >> on a container that is not running never
returns -- not even with a callback -- see
L<API::Docker::API::Containers/"The defaults follow the engine">.
B<The container must be running.> Attaching to one that has already exited
destroys its exit status on Podman, so the call checks first and croaks rather
than attaching; L</logs> is how a finished container's output is read.
C<< require_running => 0 >> attaches anyway. The check is a pre-flight one and
does not close the race against a container stopping underneath it -- see
L<API::Docker::API::Containers/"This method refuses a container that is not running">.
=head2 inspect
my $updated = $container->inspect;
Get fresh container information. Returns an
L<API::Docker::Type::ContainerInspectResponse> whatever the invocant was, so
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
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;
requires 'host';
requires 'api_version';
requires 'tls';
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
);
sub _build__socket {
my ($self) = @_;
my $host = $self->host;
my $pending = $self->_pending_connect;
my $timeout = $pending ? $pending->{timeout} : undef;
if ($host =~ m{^unix://(.+)$}) {
my $path = $1;
$log->debugf("Connecting to Unix socket: %s", $path);
my $sock = IO::Socket::UNIX->new(
Peer => $path,
Type => SOCK_STREAM,
$timeout ? (Timeout => $timeout) : (),
);
unless ($sock) {
# Asked before anything else can touch $@ or $!, which is the whole of
# the evidence; see _connect_expired.
$self->_croak_connect_timeout($pending, 'unix://' . $path)
if $self->_connect_expired($timeout);
croak "Cannot connect to Unix socket $path: $!";
}
return $sock;
}
elsif ($host =~ m{^tcp://([^:]+):(\d+)$}) {
my ($addr, $port) = ($1, $2);
unless ($self->tls) {
$log->debugf("Connecting to TCP %s:%s", $addr, $port);
my $sock = IO::Socket::INET->new(
PeerAddr => $addr,
PeerPort => $port,
Proto => 'tcp',
$timeout ? (Timeout => $timeout) : (),
);
unless ($sock) {
$self->_croak_connect_timeout($pending, $addr . ':' . $port)
if $self->_connect_expired($timeout);
croak "Cannot connect to $addr:$port: $!";
}
return $sock;
}
# Built before the connection is opened: a cert_path that names nothing,
# or half a client certificate, is a configuration mistake and the caller
# should hear about it as one rather than as a handshake failure.
my %ssl = $self->_ssl_options($addr);
$log->debugf("Connecting to TCP %s:%s over TLS (verification %s)",
$addr, $port, $self->tls_insecure ? 'off' : 'on');
my $sock = IO::Socket::SSL->new(
PeerAddr => $addr,
PeerPort => $port,
Proto => 'tcp',
$timeout ? (Timeout => $timeout) : (),
%ssl,
);
unless ($sock) {
$self->_croak_connect_timeout($pending, $addr . ':' . $port . ' (TLS)')
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# Whether the connect that has just failed failed because the bound fired.
# Asked with nothing in between, because $@ and $! are the whole of the
# evidence and both are global.
#
# $@ rather than errno: IO::Socket writes 'connect: timeout' there, and only
# there, when its own select() ran out -- measured, against a host that drops
# SYNs, where $! is ETIMEDOUT, which the kernel also produces on its own after
# two minutes with no Timeout set at all.
#
# EAGAIN is the second shape and belongs to unix:// alone. Measured against a
# listener whose backlog is full: with no Timeout the connect blocks
# indefinitely (still blocked after 8s), and with one it fails at once with
# EAGAIN, because IO::Socket does the timed connect non-blocking and an
# AF_UNIX connect has no in-progress state to wait on. So on that transport
# the option does not wait, it refuses -- but a connect that failed with
# EAGAIN is still one the bound ended, and reporting it as anything else would
# name a cause the caller cannot act on.
sub _connect_expired {
my ($self, $timeout) = @_;
return 0 unless $timeout;
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# Every byte of a response is taken off the handle by _pull and by nothing
# else, and every reader below is served out of the buffer _pull fills. That
# is not an optimisation, it is the only shape that works (karr k60).
#
# What forced it: perl's read() is fread-shaped. It loops until it has the
# LENGTH it was asked for or the stream ends -- it does not return what has
# arrived. Measured on an AF_UNIX socketpair whose peer writes 6 bytes, waits
# half a second, writes 6 more and closes: read($sock, $buf, 65536) came back
# with 12 after 0.90s, having waited for the close, while sysread came back
# with 6 in 0.00s. On the endpoints with neither a Content-Length nor chunked
# encoding -- attach, logs(follow), exec/start, all
# application/vnd.docker.raw-stream -- the reader asks for $READ_SIZE, so
# read() delivered nothing to an on_frame/on_chunk callback until 64K had
# piled up or the daemon hung up. On a stream that never ends it would deliver
# nothing at all. The POD promised those callbacks the bytes as they arrive,
# and that promise was not kept.
#
# Why it could not be fixed at the one site that had the bug: _read_head read
# the status line and the headers with <$sock>, and PerlIO reads ahead. The
# bytes past the header block were sitting in a buffer this code cannot reach
# -- there is no supported way to take them back out; ungetc is layer-
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# only the keys are sorted.
for my $item (ref $v eq 'ARRAY' ? @$v : ($v)) {
next unless defined $item;
push @pairs, _uri_encode($k) . '='
. _uri_encode(ref $item eq 'HASH' ? encode_json($item) : $item);
}
}
$url_path .= '?' . join('&', @pairs) if @pairs;
}
$log->debugf("%s %s", $method, $url_path);
my $request = "$method $url_path HTTP/1.1\r\n";
$request .= "Host: localhost\r\n";
$request .= "Connection: close\r\n";
$request .= "User-Agent: API-Docker\r\n";
if (defined $body_content) {
$request .= "Content-Type: $content_type\r\n";
$request .= "Content-Length: " . length($body_content) . "\r\n";
}
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
my $ok = eval { $response = $handler
? $self->_read_streaming_response($sock, $method, $handler, $ctx)
: $self->_read_response($sock, $method, $ctx); 1 };
my $err = $@;
close $sock;
$self->_clear_socket;
die $err unless $ok;
my ($status_code, $status_text, $headers, $body, $summary) = @$response;
$log->debugf("Response: %s %s", $status_code, $status_text);
# The status line and the response headers are metadata the return value
# cannot carry: it is the decoded body and nothing else, so 204 and 304 are
# both undef and a header holding the payload -- HEAD
# /containers/{id}/archive answers with an empty body and
# X-Docker-Container-Path-Stat -- is unreachable. They go into a hash the
# caller supplies, so no existing caller's return shape changes. Filled
# before the croak below, so an eval'ing caller can still read the status.
if (my $out = $opts{response}) {
%$out = (
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
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
# Read until the daemon closes. This used to be a `local $/; <$sock>` slurp;
# it is a loop over the same primitive as the other two branches now, which
# is what karr k60 needed and what the timeout wanted anyway -- with $/ undef
# a whole body and a truncated one are both just bytes, so the slurp's own
# result could never say which it was. This is the path karr k52's hang is
# on, an attach whose buffered frames arrive and whose socket then never
# closes.
#
# And the one shape with no completeness check to make: the response
# announced no end, so the close IS the end (karr k64). Treating an EOF here
# as truncation would make every attach, every logs(follow) and every
# exec/start fail on the daemon hanging up, which is how all three finish.
my $body = '';
# What a timeout hands over instead of dropping; see the content-length
# branch above.
local $ctx->{partial} = \$body;
while (1) {
my ($n, $buf) = $self->_read_bytes($sock, $READ_SIZE, $ctx);
last unless $n;
$body .= $buf;
}
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
my $chunk_size = $self->_assert_chunk_header($ctx, $chunk_header);
last if $chunk_size == 0;
my $read = 0;
while ($read < $chunk_size) {
my ($n, $buf) = $self->_read_bytes($sock, $chunk_size - $read, $ctx);
last unless $n;
$read += $n;
# Fed per read rather than per completed chunk. A chunk is the
# daemon's framing, not the caller's -- the engine is free to send an
# hour of log output as one chunk -- so waiting for the whole of one
# would reintroduce exactly the buffering this path exists to avoid.
$more = $feed->($buf);
last unless $more;
}
# Every truncation check on this path is guarded by $more, and that is
# the whole of what distinguishes the two ways a streamed chunk ends
# early: the daemon ran out, or the callback said stop. A caller that
# stopped left the rest of the chunk unread on purpose (karr k64).
$self->_croak_truncated($ctx, phase => 'chunk-data', piece => 'a chunk',
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
=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>
=item * Automatic connection management
=back
Consuming classes must provide C<host>, C<api_version>, C<tls>, C<cert_path>
and C<tls_insecure> attributes. The last three are read only by the C<tcp://>
branch of the socket builder, and only when TLS is asked for, but the contract
is stated once rather than probed for at connect time.
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
=head3 There is no default, and no per-endpoint default either
Off unless asked for, everywhere. Whether a silence is a stall or normal is a
property of the workload rather than of the endpoint: C</build> with a large
context is legitimately quiet for as long as C</events> is, and a built-in
default on C<attach> would kill a perfectly healthy session at an idle shell
prompt. So no existing call changes behaviour, and picking the number is the
caller's -- who is the only one who knows what the request is for.
For the two endpoints above, if you want a figure to start from: a couple of
seconds is right for C<attach> or C<logs> used to collect what is already
there, and something above the daemon's own emit interval -- Docker sends a
stats reading about once a second -- for C<stats>.
=head3 What happens when it expires
The request croaks, on every path, with an L<API::Docker::Error::Timeout>. It
never returns a truncated response: a short body satisfies every return shape
this role promises and would be indistinguishable from a complete one. The
exception carries what did arrive -- C<< ->partial >> for a buffered request,
C<< ->summary >> for a streamed one -- so collecting what there is and then
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
measured rather than assumed:
=over
=item * C<tcp://> -- a real bound. Against a host that drops SYNs, an unbounded
connect waits for the kernel's own timeout, which on Linux is over two
minutes; C<< connect_timeout => 2 >> gave up after 2.00s. This is the case the
option exists for.
=item * C<unix://> -- a bound, but it does not wait. A connect to a Unix socket
whose listen backlog is full blocks: measured against a listener with
C<< Listen => 1 >> and nobody accepting, still blocked after 8 seconds. With a
C<connect_timeout> set it fails at once instead, with C<EAGAIN> -- because
C<IO::Socket> performs a timed connect non-blocking, and an C<AF_UNIX> connect
has no in-progress state to wait on. So the hang is gone, at the price of not
tolerating even a momentary backlog. A socket path that does not exist is
C<ENOENT> either way and is not affected.
=item * TLS -- bounds the TCP connect only. The handshake that follows it runs
on the connected socket, before L</read_timeout>'s C<SO_RCVTIMEO> is applied,
and is not covered by either.
=back
An expiry croaks with an L<API::Docker::Error::Timeout> carrying
C<< ->phase >> C<'connect'>, C<< ->timeout >> the value that expired and an
empty C<< ->partial >> -- there is no response to have part of. Every other
connect failure croaks with the plain string it always did: a refused
connection, a missing socket path and a rejected certificate are diagnoses,
not timeouts, and rewriting them as one would name a cause the caller cannot
act on.
=head2 Streaming a response as it arrives
Without one of these options a request is read whole, then parsed. That is
right for a request/response endpoint and wrong for every endpoint whose point
is that it keeps going: C<< logs(follow => 1) >>, C</events> with no C<until>
and C</containers/{id}/stats> with no C<< stream => 0 >> never return, because
the daemon never closes and there is nothing else to wait for.
A callback is half the answer -- it decides what to do with each unit, and it
can stop. L</"Bounding a request that never ends"> is the other half, for the
stream that stops arriving without ever ending.
Pass a callback and the body is handed over piece by piece instead:
my $summary = $client->get('/events',
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
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
knows about
=back
Passing two of them croaks before the request is sent: they are three shapes
different endpoints have, not three views of one stream.
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
=head3 How often the callback is called
Once per unit the daemon has finished sending, as soon as the bytes that
complete it have arrived -- not once per read of a fixed size, and not once
at the end.
That is worth stating because it was not true before karr k60. The reads were
C<read()>, which is C<fread>-shaped: it loops until it has the length it was
asked for or the stream ends, rather than returning what has arrived. On the
raw-stream endpoints -- C<attach>, C<< logs(follow => 1) >>, C<exec/start>,
which carry neither a C<Content-Length> nor chunked encoding -- the reader
asks for 64K, so nothing reached the callback until 64K had accumulated or the
daemon hung up. On a stream that never ends, nothing reached it at all.
Measured on an C<AF_UNIX> socket pair with no daemon involved, a peer writing
three frames 0.15s apart and then closing:
before: 1 call at 0.45s (the moment it closed)
after: 3 calls at 0.15s, 0.30s, 0.45s
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
a value.
The one thing here that is B<not> raised as an object: a connection that
closed without a single byte of a status line still croaks with the plain
C<No response from Docker daemon> string it always has. Nothing about it was
ever silent, and it is a message callers may be matching on.
=head3 Where an end of stream is still the end
A body delimited by nothing but the close. C<attach>,
C<< logs(follow => 1) >>, C</exec/{id}/start> -- the whole
C<application/vnd.docker.raw-stream> family -- carry neither a
C<Content-Length> nor chunked encoding, so the response announces no end and
there is nothing for a short one to be short of. That is how every one of them
finishes, and treating it as truncation would break all of them.
Their B<heads> are another matter and are checked like every other head. An
engine writes those two by hand rather than through its HTTP server, so it is
worth saying that they are well-formed: both answer with C<HTTP/1.1 200 OK>, a
single C<Content-Type> line and the blank line, measured on Docker 29.7.2 and
on rootless Podman 5.8.4. So does every other shape either of them produces --
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
among them, and then sends nothing -- reading it would block on bytes that
never arrive. Measured against Podman 5.4.2 (API 1.41),
C<< HEAD /containers/{id}/archive >> in fact announces no length at all, only
C<X-Docker-Container-Path-Stat> -- but an engine that does announce one is not
waited on either.
Options: C<params>, C<headers> and C<response> as for L</get>.
=head2 stream_frames
my $frames = $client->stream_frames('GET', "/containers/$id/logs", %opts);
Perform a request against one of the engine's framed endpoints
(C<< /containers/{id}/logs >>, C<< /exec/{id}/start >>) and return an ArrayRef
of frames:
[ { 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;
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
the whole ArrayRef being returned at the end; see below.
=back
=head2 Following a framed stream
With C<on_frame> the frames are handed over as they arrive and the return
value is the summary HashRef described in L</"Streaming a response as it
arrives">, not an ArrayRef:
my $summary = $client->stream_frames('GET', "/containers/$id/logs",
params => { follow => 1, stdout => 1, stderr => 1 },
on_frame => sub {
my ($frame, $stop) = @_;
print $frame->{data};
$stop->() if $frame->{data} =~ /listening on/;
},
);
This is the only way to use C<< follow => 1 >> at all: without it the request
does not return until the container exits.
lib/API/Docker/Role/HTTP.pm view on Meta::CPAN
=head2 Detecting a framed stream
A container created without a TTY produces the Docker stream format -- an
8-byte header per frame (byte 0 the stream type, bytes 4-7 a big-endian uint32
payload length) followed by that many payload bytes. With a TTY there is no
header and the payload is raw pty output.
The engine is supposed to distinguish the two with the response C<Content-Type>
(C<application/vnd.docker.multiplexed-stream> against
C<application/vnd.docker.raw-stream>), but that signal is not dependable.
Measured against Podman 5.4.2 (API 1.41): C<< GET /containers/{id}/logs >>
sends no C<Content-Type> at all, for either kind of container, and
C<< POST /exec/{id}/start >> sends C<application/vnd.docker.raw-stream> for
both -- including the non-TTY exec whose body is in fact multiplexed. Trusting
the header would therefore hand frame headers to the caller on that engine.
The framing is decided from the bytes instead. The body is walked as frames:
each header must have a stream type of 0, 1 or 2, three zero bytes after it,
and a payload length that leaves at least that many bytes in the buffer. The
body is treated as framed only when the walk consumes it exactly and yields at
least one frame; anything else is returned as a single C<raw> frame.
lib/API/Docker/Role/Using.pm view on Meta::CPAN
written, decide it at the call:
my $r = %bounds ? $docker->containers->using(%bounds) : $docker->containers;
=back
An odd number of arguments croaks too, before the pairs are read.
=head2 What has no clone of its own
B<The entity classes.> C<< $container->logs >> and its neighbours are
one-line delegations to the resource class, and they hold the container's own
daemon fields -- every one of them, verbatim -- rather than a call surface, so
a clone would have to copy a record whose shape is the daemon's. The bound
belongs where the request is built:
$docker->containers->using(read_timeout => 5)->logs($container->id);
B<The client.> C<< $docker->using(...) >> would be a second client sharing
one connection state and one negotiated API version with the first. The
client already takes both bounds as constructor arguments, which is the level
it works at.
=head2 using
my $bounded = $docker->containers->using(read_timeout => 5);
lib/API/Docker/Type/BuildInfo.pm view on Meta::CPAN
=head2 id
Undocumented upstream. The build stream captured in
F<t/fixtures/images_build_stream.ndjson> carries no C<id> at all. The same
field on a pull, L<API::Docker::Type::CreateImageInfo/id>, names the layer
each event is about. Serialised as C<id> -- spelled out, because deriving it
from the Perl name would produce C<Id>.
=head2 stream
Undocumented upstream. The build log the way the daemon writes it, one line
per event with the newline included -- C<< {"stream":"STEP 1/2: FROM
alpine:3\n"} >> opens F<t/fixtures/images_build_stream.ndjson>. Nine of that
capture's ten events are this field; the tenth is L</aux>. Serialised as
C<stream> -- spelled out, because deriving it from the Perl name would
produce C<Stream>.
=head2 error
Errors encountered during the operation.
lib/API/Docker/Type/ClusterVolume/Info.pm view on Meta::CPAN
package API::Docker::Type::ClusterVolume::Info;
# ABSTRACT: Information about the global status of the volume
our $VERSION = '0.004';
use API::Docker::Type;
use API::Docker::Type::Topology;
use namespace::clean;
docker capacity_bytes => Int, since => '1.44';
docker volume_context => { Str, Str }, since => '1.44';
docker volume_id => Str, wire => 'VolumeID', since => '1.44';
docker accessible_topology => [ 'Topology' ], since => '1.44';
1;
__END__
=pod
=encoding UTF-8
lib/API/Docker/Type/ClusterVolume/Info.pm view on Meta::CPAN
=head2 volume_id
The ID of the volume as returned by the CSI storage plugin. This is distinct
from the volume's ID as provided by Docker. This ID is never used by the
user when communicating with Docker to refer to this volume. If the ID is
blank, then the Volume has not been successfully created in the plugin yet.
Serialised as C<VolumeID> -- spelled out, because deriving it from the Perl
name would produce C<VolumeId>.
=head2 accessible_topology
The topology this volume is actually accessible from. See
L<API::Docker::Type::Topology>.
=head1 SUPPORT
=head2 Issues
Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/p5-api-docker/issues>.
=head1 CONTRIBUTING
lib/API/Docker/Type/ClusterVolumeSpec/AccessMode.pm view on Meta::CPAN
a Block-type volume. Intentionally empty.
=head2 secrets
Swarm Secrets that are passed to the CSI storage plugin when operating on
this volume. See
L<API::Docker::Type::ClusterVolumeSpec::AccessMode::Secret>.
=head2 accessibility_requirements
Requirements for the accessible topology of the volume. These fields are
optional. For an in-depth description of what these fields mean, see the CSI
specification. See
L<API::Docker::Type::ClusterVolumeSpec::AccessMode::AccessibilityRequirements>.
=head2 capacity_range
The desired capacity that the volume should be created with. If empty, the
plugin will decide the capacity. See
L<API::Docker::Type::ClusterVolumeSpec::AccessMode::CapacityRange>.
lib/API/Docker/Type/ClusterVolumeSpec/AccessMode/AccessibilityRequirements.pm view on Meta::CPAN
package API::Docker::Type::ClusterVolumeSpec::AccessMode::AccessibilityRequirements;
# ABSTRACT: Requirements for the accessible topology of the volume
our $VERSION = '0.004';
use API::Docker::Type;
use API::Docker::Type::Topology;
use namespace::clean;
docker requisite => [ 'Topology' ], since => '1.44';
docker preferred => [ 'Topology' ], since => '1.44';
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
API::Docker::Type::ClusterVolumeSpec::AccessMode::AccessibilityRequirements - Requirements for the accessible topology of the volume
=head1 VERSION
version 0.004
=head1 DESCRIPTION
Generated from the inline C<AccessibilityRequirements> schema of
C<ClusterVolumeSpec.AccessMode> in C<spec/v1.51.yaml>.
These fields are optional. For an in-depth description of what these fields
mean, see the CSI specification.
=head2 requisite
A list of required topologies, at least one of which the volume must be
accessible from. See L<API::Docker::Type::Topology>.
=head2 preferred
A list of topologies that the volume should attempt to be provisioned in.
See L<API::Docker::Type::Topology>.
=head1 SUPPORT
=head2 Issues
Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/p5-api-docker/issues>.
=head1 CONTRIBUTING