Files
hubmanager/CLAUDE.md

216 lines
8.7 KiB
Markdown
Raw Normal View History

# hubmanager — AI Session Context
This file provides context for AI assistants working on this project.
## Project Summary
**hubmanager** is a single-file Bash CLI tool to manage Docker Registry images remotely.
It supports both Docker Hub and any self-hosted Docker Registry v2 API, with automatic
authentication detection (bearer token or HTTP basic auth).
---
## What Changed (Session 2026-02-21)
### Encrypted config values (`--encrypt`)
Added `openssl` AES-256-CBC encryption for passwords stored in the config file.
`openssl` is an **optional** dependency — it is only required when `enc:` prefixed values
are present in the config, or when `login --encrypt` is used.
**New functions** (Encryption helpers section):
| Function | Purpose |
| --- | --- |
| `_require_openssl()` | Die with a clear message if `openssl` is not installed |
| `_prompt_master_pass()` | Prompt once per session via `/dev/tty`; cache in `HM_MASTER_PASS` |
| `_prompt_set_master_pass()` | Prompt + confirm a new passphrase (used by `login --encrypt`) |
| `_encrypt_value(plaintext)` | AES-256-CBC encrypt → base64 ciphertext (no newlines) |
| `_decrypt_value(ciphertext)` | Decrypt base64 ciphertext → plaintext; die on wrong passphrase |
**Passphrase security**: passed to `openssl` via a `mktemp` file (`-pass file:`) to avoid
exposure in the process argument list (`ps aux`). The temp file is registered in
`HM_TMPFILES` and removed on exit.
**Config format**: encrypted values use an `enc:` prefix, e.g.:
```txt
PASSWORD=enc:U2FsdGVkX1+...base64ciphertext...
REGISTRY_PROD_PASSWORD=enc:U2FsdGVkX1+...
```
Both `load_config()` and `resolve_registry_alias()` detect the prefix and call
`_decrypt_value` transparently.
**Version bump**: `0.1.0``0.2.0`
---
## What Was Built (Session 2025-02-21)
### Primary file
`hubmanager` — executable Bash script, ~680 lines, no mandatory dependencies beyond `curl`, `jq`, Bash 4+.
`openssl` is required only when encrypted config values are used.
### Subcommands implemented
| Command | Description |
| --- | --- |
| `login` | Test credentials against a registry; `--save` writes to `~/.hubmanager.conf` |
| `list` | List repositories (`_catalog` for self-hosted; Hub REST API for Docker Hub) |
| `tags` | List tags for an image with pagination |
| `inspect` | Show manifest digest, size, OS/arch, layers, labels; multi-arch support |
| `delete` | Resolve tag → digest → `DELETE`; requires confirmation or `--yes` |
| `copy` | Copy/retag within or across registries; blob mount for same-registry retag |
| `prune` | Delete outdated tags sorted by image creation date |
### Supporting files
- `PLAN.md` — implementation plan written before coding
- `README.md` — full user-facing documentation with examples
- `tests/fixtures/` — sample JSON responses for testing (token, catalog, tags, manifests, config blob)
---
## Architecture
### Single-file, section-based layout
```txt
#!/usr/bin/env bash
set -euo pipefail
# --- Constants ---
# --- Global state ---
# --- Output / Formatting helpers ---
# --- Dependency check ---
# --- Encryption helpers --- _encrypt_value(), _decrypt_value(), _prompt_master_pass()
# --- Config loading ---
# --- HTTP helpers --- raw_http(), get_response_header()
# --- Authentication --- probe_registry_auth(), get_bearer_token(), make_auth_header()
# --- Registry request --- registry_request() ← main HTTP wrapper
# --- Subcommands --- cmd_login/list/tags/inspect/delete/copy/prune
# --- Global arg parsing --- parse_global_args()
# --- Main dispatcher --- main()
main "$@"
```
### Key design decisions
1. **`raw_http METHOD URL [curl-args]`** — thin curl wrapper. Writes headers to a temp file
(`HM_LAST_HEADERS_FILE`), sets `HM_LAST_HTTP_CODE`, outputs body to stdout.
2. **`registry_request METHOD REGISTRY PATH [options]`** — higher-level wrapper that:
- Probes auth mode via `probe_registry_auth` (result cached per registry)
- Fetches and caches bearer tokens per `"registry|scope"` key
- Retries once on 401 using the challenge scope from `WWW-Authenticate`
- Calls `_handle_http_error` on non-2xx (unless `--no-die`)
3. **`make_auth_header REGISTRY SCOPE [USER] [PASS]`** — returns the correct
`Authorization:` header string for either bearer or basic auth.
4. **Authentication flow**:
- `GET /v2/` → parse `WWW-Authenticate` → detect `bearer` or `basic`
- Bearer: `GET <realm>?scope=S&service=S` with `-u user:pass` → cache `.token`
- Basic: construct `Authorization: Basic <base64>` directly
- Tokens cached in `HM_TOKEN_CACHE["registry|scope"]` with epoch expiry
5. **Docker Hub differences**:
- `list` uses `hub.docker.com/v2/repositories/<user>/` (not `_catalog`)
- `delete` and `prune` are blocked (Hub v2 API doesn't support deletion)
- Hub REST API uses a separate JWT from `hub.docker.com/v2/users/login`
6. **Config file** (`~/.hubmanager.conf`):
- `KEY=VALUE` format, parsed with `while IFS='=' read` (not `source`)
- Supports named registry aliases: `REGISTRY_<ALIAS>_URL/USERNAME/PASSWORD`
- Aliases are resolved in `resolve_registry_alias()` before any operations
7. **Prune logic**: fetches all tags → filters by `--exclude` pattern → inspects
each tag's config blob for creation date → sorts oldest-first → deletes oldest
(beyond `--keep` count or before `--older-than` days). Supports `--dry-run`.
8. **Copy blob transfer**:
- Same registry, different repos → attempt cross-repo blob mount (`POST ?mount=`)
- Blob already at destination (`HEAD` returns 200) → skip
- Otherwise → download to temp file → `POST` initiate upload → `PUT` with digest
9. **Encrypted config values** (v0.2.0):
- `login --save --encrypt` prompts for a master passphrase (with confirmation), encrypts
the password with `openssl enc -aes-256-cbc -pbkdf2 -a`, and writes `PASSWORD=enc:<b64>`.
- Passphrase is passed to `openssl` via a temp file (`-pass file:`) — never via argv or env.
- `load_config` and `resolve_registry_alias` both check for the `enc:` prefix and call
`_decrypt_value`, which triggers `_prompt_master_pass` (once per session, cached in
`HM_MASTER_PASS`).
- `openssl` is an optional dependency: not checked at startup, only on first `enc:` encounter.
---
## Global Variables (key ones)
| Variable | Purpose |
| --- | --- |
| `HM_REGISTRY` | Active registry URL (no trailing slash) |
| `HM_USERNAME` / `HM_PASSWORD` | Active credentials |
| `HM_CONFIG_FILE` | Path to config file |
| `HM_OPT_JSON/VERBOSE/QUIET/NO_COLOR` | Output flags |
| `HM_AUTH_MODE["registry"]` | `bearer`, `basic`, or `none` |
| `HM_AUTH_REALM["registry"]` | Bearer token endpoint URL |
| `HM_TOKEN_CACHE["registry\|scope"]` | Cached bearer token |
| `HM_TOKEN_EXPIRY["registry\|scope"]` | Token expiry (epoch seconds) |
| `HM_MASTER_PASS` | Master passphrase for `enc:` config values (session-cached) |
| `HM_LAST_HTTP_CODE` | HTTP status of most recent request |
| `HM_LAST_HEADERS_FILE` | Temp file path with response headers |
| `HM_TMPFILES` | Array of temp files, cleaned up via `trap EXIT` |
| `MANIFEST_ACCEPT_HEADERS` | Array of `-H Accept:` args for all manifest types |
---
## Adding a New Subcommand
1. Write a `cmd_<name>()` function following the same pattern:
- Parse args with a `while case` loop; handle `--help`
- Set `HM_REGISTRY` default if empty
- Use `registry_request` for API calls
- Check `HM_OPT_JSON` and branch for table vs. JSON output
2. Add it to the `case` block in `main()`:
```bash
newcmd) cmd_newcmd "$@" ;;
```
3. Add it to `show_usage()`.
4. Run `shellcheck hubmanager` — it must pass with zero warnings.
---
## Exit Codes
| Code | Meaning |
| --- | --- |
| 0 | Success |
| 1 | General / usage error (`die`) |
| 2 | Authentication failure (`die_auth`) |
| 3 | Not found / 404 (`die_notfound`) |
| 4 | Permission denied / 403 (`die_permission`) |
| 5 | Registry server error (`die_server`) |
| 6 | Network error (`die_network`) |
| 7 | Operation not supported (`die_notsup`) |
---
## Known Limitations / Future Work
- **Garbage collection** after `delete`/`prune`: the registry only frees disk space
after running `registry garbage-collect`. This tool does not trigger GC; it must
be run separately on the registry host.
- **Docker Hub rate limits**: the `inspect`-per-tag loop in `prune` makes one API
call per tag, which can hit pull rate limits on Docker Hub (prune is blocked on
Hub anyway).
- **Token scope granularity**: the retry-on-401 flow handles most scope mismatches,
but registries that require compound scopes (e.g. `pull,push` together) may need
explicit `--scope` passing inside `registry_request` calls.
- **No `bats` tests yet**: `tests/fixtures/` contains sample API responses. Wire up
`bats-core` tests that mock `curl` with a shell function and feed fixture data.