Files
hubmanager/CLAUDE.md
magdev 661de2f3d8 feat: initial implementation of hubmanager v0.1.0
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>
2026-02-21 14:37:31 +01:00

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 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

#!/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():

    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.