API-Docker

 view release on metacpan or  search on metacpan

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

---
name: api-docker-core
description: "Use when working on the API::Docker distribution's client architecture — the HTTP transport role and its socket handling (unix://, tcp://, TLS), a resource API under API::Docker::API::*, how list/inspect wrap a daemon response into a ...
---

# API::Docker — architecture and invariants

A pure-Perl client for the Docker Engine HTTP API. It speaks HTTP/1.1 directly
over the daemon's socket and never shells out to the `docker` binary, so any
engine serving that API works (Podman's rootless socket needs nothing but
`DOCKER_HOST`).

## The three layers

```
API::Docker                     client; host, api_version, negotiation
  └─ with API::Docker::Role::HTTP    _request + get/post/put/delete_request
  └─ ->images / ->containers / ->networks / ->volumes / ->system / ->exec
        API::Docker::API::*      one class per resource, holds `client`
          └─ _wrap / _wrap_list  → $class->from_data($data, client => ...)
                                    on an API::Docker::Type::* class, with
                                    API::Docker::Role::Entity::* composed
                                    onto that class at load time
```

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

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

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

## Invariants

- **`list` and `inspect` return generated `API::Docker::Type::*` objects**
  (via `_wrap`/`_wrap_list`), everything else returns the raw daemon
  response. Which class depends on the resource: `Networks`, `Volumes`,
  `Plugins`, `Secrets` and `Configs` answer both calls with the same class
  (one swagger definition each — `API::Docker::Type::Network`, etc.), while
  `Containers` and `Images` each have two, with different fields —
  `ContainerSummary`/`ContainerInspectResponse`,
  `ImageSummary`/`ImageInspect` (see `API::Docker::API::Containers/"The two
  container shapes"`). Which resources have one class vs. two is read off
  `spec/v1.51.yaml`, not assumed — a future swagger could add a second
  definition to a resource that only has one today.
- **The seven hand-written entity classes are gone (k84)** —
  `API::Docker::{Container,Image,Network,Volume,Plugin,Secret,Config}`. The
  files still ship, but only as stubs that croak on load and on every method
  call (k92), so installing a new release overwrites the working copy an
  older one left on disk instead of leaving it to shadow the release. Never
  write code against them or treat their POD as current; the objects the
  daemon answers with are the `API::Docker::Type::*` classes above.
- **The composed `client` on an entity is a `weak_ref`**, declared by
  `API::Docker::Role::Entity` and composed into every wrapped
  `API::Docker::Type::*` alongside the resource-specific
  `API::Docker::Role::Entity::*` role. `API::Docker->new->images->list`
  leaves every returned entity with `client => undef`, and the next
  `$image->remove` dies on an undefined invocant. The client must stay in a
  live variable — in library code, in examples, and in tests.
- **Query-string booleans are normalised to `1`/`0`** (`$opts{all} ? 1 : 0`);
  **JSON-body booleans are `\1`/`\0`** (see `Exec::start`), because the engine
  type-checks the body but not the query string.
- **A `params` value that is a hashref is JSON-encoded automatically**, but
  `filters` specifically goes through `API::Docker::Role::Filters`, which
  normalises it into the map-of-string-to-array-of-string shape the engine
  wants (wraps a bare scalar in an array, stringifies numbers, turns a JSON
  or `\1`/`\0` boolean into `'true'`/`'false'`) and croaks on anything else —
  another ref, `undef`, an empty string. Pass
  `filters => { dangling => ['true'] }` and let the role do the rest;
  encoding it by hand double-encodes it.
- **Extra headers go through `headers =>`**, which strips CR/LF. Never
  concatenate a header into the request string.
- `_uri_encode` deliberately leaves `/` and `:` raw so image names survive in
  the path (`/images/library/nginx:1.25/push`).
- `sub push` and `sub kill` shadow Perl builtins inside their packages — that
  is why `namespace::clean` is loaded; always call them as methods.

## Entities: generated types with composed methods, not hand-written classes

`_wrap`/`_wrap_list` build the object with `$class->from_data($data, client
=> $self->client)` — never `new`. A daemon response and a caller-built
object are different name spaces: `from_data` reads only the swagger's own



( run in 0.633 second using v1.01-cache-2.11-cpan-f9ab5d97e31 )