You've already forked hubmanager
Add a Bash CLI tool to manage Docker Registry images remotely. Supports Docker Hub and self-hosted Docker Registry v2 API with automatic auth detection (bearer token or HTTP basic auth). Subcommands: login, list, tags, inspect, delete, copy, prune Dependencies: curl, jq, bash 4+ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
168 lines
6.5 KiB
Markdown
168 lines
6.5 KiB
Markdown
# 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 <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
|
|
|
|
---
|
|
|
|
## 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_<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.
|