# 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 Was Built (Session 2025-02-21) ### Primary file `hubmanager` — executable Bash script, ~600 lines, no dependencies beyond `curl`, `jq`, Bash 4+. ### 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 --- # --- 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 ?scope=S&service=S` with `-u user:pass` → cache `.token` - Basic: construct `Authorization: Basic ` directly - Tokens cached in `HM_TOKEN_CACHE["registry|scope"]` with epoch expiry 5. **Docker Hub differences**: - `list` uses `hub.docker.com/v2/repositories//` (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__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 --- ## 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_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_()` 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.