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>
6.5 KiB
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 codingREADME.md— full user-facing documentation with examplestests/fixtures/— sample JSON responses for testing (token, catalog, tags, manifests, config blob)
Architecture
Single-file, section-based layout
#!/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
-
raw_http METHOD URL [curl-args]— thin curl wrapper. Writes headers to a temp file (HM_LAST_HEADERS_FILE), setsHM_LAST_HTTP_CODE, outputs body to stdout. -
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_erroron non-2xx (unless--no-die)
- Probes auth mode via
-
make_auth_header REGISTRY SCOPE [USER] [PASS]— returns the correctAuthorization:header string for either bearer or basic auth. -
Authentication flow:
GET /v2/→ parseWWW-Authenticate→ detectbearerorbasic- Bearer:
GET <realm>?scope=S&service=Swith-u user:pass→ cache.token - Basic: construct
Authorization: Basic <base64>directly - Tokens cached in
HM_TOKEN_CACHE["registry|scope"]with epoch expiry
-
Docker Hub differences:
listuseshub.docker.com/v2/repositories/<user>/(not_catalog)deleteandpruneare blocked (Hub v2 API doesn't support deletion)- Hub REST API uses a separate JWT from
hub.docker.com/v2/users/login
-
Config file (
~/.hubmanager.conf):KEY=VALUEformat, parsed withwhile IFS='=' read(notsource)- Supports named registry aliases:
REGISTRY_<ALIAS>_URL/USERNAME/PASSWORD - Aliases are resolved in
resolve_registry_alias()before any operations
-
Prune logic: fetches all tags → filters by
--excludepattern → inspects each tag's config blob for creation date → sorts oldest-first → deletes oldest (beyond--keepcount or before--older-thandays). Supports--dry-run. -
Copy blob transfer:
- Same registry, different repos → attempt cross-repo blob mount (
POST ?mount=) - Blob already at destination (
HEADreturns 200) → skip - Otherwise → download to temp file →
POSTinitiate upload →PUTwith digest
- Same registry, different repos → attempt cross-repo blob mount (
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
-
Write a
cmd_<name>()function following the same pattern:- Parse args with a
while caseloop; handle--help - Set
HM_REGISTRYdefault if empty - Use
registry_requestfor API calls - Check
HM_OPT_JSONand branch for table vs. JSON output
- Parse args with a
-
Add it to the
caseblock inmain():newcmd) cmd_newcmd "$@" ;; -
Add it to
show_usage(). -
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 runningregistry 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 inprunemakes 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,pushtogether) may need explicit--scopepassing insideregistry_requestcalls. - No
batstests yet:tests/fixtures/contains sample API responses. Wire upbats-coretests that mockcurlwith a shell function and feed fixture data.