# hubmanager A Bash CLI tool to manage Docker Registry images remotely. Supports Docker Hub and any self-hosted Docker Registry v2 API, with flexible authentication. ## Requirements - **Bash** 4.0+ - **curl** - **jq** - **openssl** *(optional — required only when using encrypted config values)* - **keyctl** *(optional — from `keyutils`; enables passphrase caching across invocations)* ## Installation ```bash # System-wide sudo install -m 755 hubmanager /usr/local/bin/hubmanager # User-local install -m 755 hubmanager ~/bin/hubmanager ``` ## Quick Start ```bash # Test your credentials and save them hubmanager login --registry https://registry.example.com \ --user admin --password secret --save # Save with password encrypted at rest hubmanager login --registry https://registry.example.com \ --user admin --password secret --save --encrypt # List all repositories hubmanager list # List tags for an image hubmanager tags myuser/myapp # Inspect an image hubmanager inspect myuser/myapp:latest # Delete an old tag hubmanager delete myuser/myapp:v1.0.0 # Prune old tags (keep the 3 most recent) hubmanager prune myuser/myapp --keep 3 --dry-run ``` ## Configuration Credentials and registry settings are stored in `~/.config/hubmanager.conf`. The file uses a simple `KEY=VALUE` format: ```bash # ~/.config/hubmanager.conf # chmod 600 ~/.config/hubmanager.conf # Default registry and credentials REGISTRY=https://registry.example.com USERNAME=admin PASSWORD=mysecretpassword # Named registry aliases (use with --registry ) REGISTRY_STAGING_URL=https://staging-registry.example.com REGISTRY_STAGING_USERNAME=deploy REGISTRY_STAGING_PASSWORD=deploytoken REGISTRY_HUB_URL=https://registry-1.docker.io REGISTRY_HUB_USERNAME=myuser REGISTRY_HUB_PASSWORD=myhubtoken ``` > The file must be readable only by the owner (`chmod 600`). hubmanager will warn if permissions are too open. Named aliases let you switch registries with a short name: ```bash hubmanager --registry staging list hubmanager --registry hub tags myuser/myapp ``` ### Encrypted credentials Use `login --save --encrypt` to store the password encrypted with AES-256-CBC. A master passphrase is prompted at save time (with confirmation) and on every subsequent invocation that reads the config file. Requires `openssl`. ```bash hubmanager login --registry https://registry.example.com \ --user admin --password secret --save --encrypt # New master passphrase: **** # Confirm master passphrase: **** # Login Succeeded — bearer auth, registry: https://registry.example.com # Credentials saved to /home/user/.config/hubmanager.conf # Password stored encrypted (AES-256-CBC). Master passphrase required on each use. ``` The config file stores an `enc:` prefixed ciphertext: ```text REGISTRY=https://registry.example.com USERNAME=admin PASSWORD=enc:U2FsdGVkX1+...base64ciphertext... ``` The `enc:` prefix also works for named alias passwords (`REGISTRY__PASSWORD`). On every command that reads the config, the master passphrase is prompted once and cached for the duration of the session. ### Passphrase caching across invocations If `keyctl` (from the `keyutils` package) is installed, the master passphrase is automatically cached in the Linux kernel keyring for 5 minutes. Subsequent commands within that window will not re-prompt. ```bash # First command prompts for passphrase, caches it for 5 min hubmanager list # Runs without prompting (within cache window) hubmanager tags myuser/myapp # Pre-cache before a scripted batch hubmanager unlock hubmanager list && hubmanager tags myuser/myapp && hubmanager inspect myuser/myapp:latest # Clear cache immediately hubmanager lock # Custom timeout (10 minutes) hubmanager unlock --cache-timeout 600 ``` If `keyctl` is not installed, passphrase caching is silently skipped — each invocation prompts as before. ## Global Options ```text hubmanager [OPTIONS] [COMMAND OPTIONS] -r, --registry Registry base URL Default: https://registry-1.docker.io -u, --user Username (overrides config file) -p, --password Password or token (overrides config file) --config Config file path (default: ~/.config/hubmanager.conf) --cache-timeout Passphrase cache TTL in seconds (default: 300) --json Output raw JSON (pipe-friendly) --no-color Disable ANSI color -v, --verbose Show HTTP request details (with auth redacted) -q, --quiet Suppress all non-error output -h, --help Show help --version Show version ``` --- ## Commands ### `login` — Test and save credentials ```text hubmanager login [--registry URL] [--user USER] [--password PASS] [--save] [--encrypt] ``` Validates credentials against the registry. Use `--save` to write them to the config file. Add `--encrypt` to store the password encrypted with AES-256-CBC (requires `openssl`). ```bash hubmanager login --registry https://registry.example.com \ --user admin --password secret --save # Login Succeeded — bearer auth, registry: https://registry.example.com # Credentials saved to /home/user/.config/hubmanager.conf ``` --- ### `unlock` — Cache master passphrase ```text hubmanager unlock [--cache-timeout SECONDS] ``` Prompts for the master passphrase and stores it in the Linux kernel keyring for the configured timeout (default: 300 seconds / 5 minutes). Useful before running a batch of commands. Requires `keyctl` (keyutils package). ```bash hubmanager unlock # hubmanager master passphrase: **** # Passphrase cached for 300s. # Custom timeout hubmanager unlock --cache-timeout 600 ``` --- ### `lock` — Clear cached passphrase ```text hubmanager lock ``` Immediately revokes the cached passphrase from the kernel keyring. ```bash hubmanager lock # Passphrase cache cleared. ``` --- ### `list` — List repositories ```text hubmanager list [--limit N] [--last REPO] ``` Lists all repositories in the registry. On **Docker Hub**, lists repositories for the authenticated user (Docker Hub restricts the `_catalog` endpoint). ```bash hubmanager list # REPOSITORY # myuser/myapp # myuser/myapi # myuser/nginx-custom # Paginate hubmanager list --limit 50 --last myuser/myapi # JSON output hubmanager list --json | jq '.repositories[]' ``` --- ### `tags` — List tags for an image ```text hubmanager tags [--limit N] [--last TAG] ``` ```bash hubmanager tags myuser/myapp # TAG # latest # v1.2.3 # v1.2.2 # develop # Official Docker Hub images hubmanager tags nginx # JSON hubmanager tags myuser/myapp --json | jq '.tags[]' ``` --- ### `inspect` — Show image details ```text hubmanager inspect : [--platform OS/ARCH] ``` Shows manifest digest, size, OS/arch, creation date, labels, and layer breakdown. ```bash hubmanager inspect myuser/myapp:latest # Image: myuser/myapp:latest # Digest: sha256:abc123... # MediaType: application/vnd.docker.distribution.manifest.v2+json # CompressedSize: 32.7 MB (34299597 bytes) # OS/Arch: linux/amd64 # Created: 2024-01-15T10:30:00.000000000Z # Labels: # maintainer=dev@example.com # version=1.2.3 # Layers: 3 # [0] sha256:1111... (27.8 MB) # [1] sha256:2222... (4.4 MB) # [2] sha256:3333... (1000.0 KB) # Multi-arch image — shows all platforms hubmanager inspect nginx:latest # Multi-arch — drill into a specific platform hubmanager inspect nginx:latest --platform linux/arm64 # Inspect by digest hubmanager inspect myuser/myapp@sha256:abc123... # JSON output (includes _digest field) hubmanager inspect myuser/myapp:latest --json | jq . ``` --- ### `delete` — Delete a tag or manifest ```text hubmanager delete : [--yes] ``` Deletes a manifest by resolving the tag to its content-addressable digest, then issuing a `DELETE`. Requires `REGISTRY_STORAGE_DELETE_ENABLED=true` on self-hosted registries. Not supported on Docker Hub. ```bash hubmanager delete myuser/myapp:v1.0.0 # About to delete: myuser/myapp @ sha256:abc123... # Registry: https://registry.example.com # Type 'yes' to confirm: yes # Deleted: myuser/myapp @ sha256:abc123... # Skip confirmation hubmanager delete myuser/myapp:v1.0.0 --yes # Delete by digest directly hubmanager delete myuser/myapp@sha256:abc123... --yes ``` --- ### `copy` — Copy or retag an image ```text hubmanager copy : : [options] ``` ```text Options: --src-registry URL Source registry (default: global --registry) --dst-registry URL Destination registry (default: global --registry) --src-user USER Source username --src-password PASS Source password --dst-user USER Destination username --dst-password PASS Destination password --platform OS/ARCH Copy only one platform from a multi-arch image ``` **Same-registry retag** — attempts cross-repo blob mount (no data transfer): ```bash hubmanager copy myuser/myapp:v1.2.3 myuser/myapp:stable ``` **Cross-registry copy** — streams blobs from source to destination: ```bash hubmanager copy myuser/myapp:latest \ --src-registry https://registry-1.docker.io \ --dst-registry https://registry.example.com \ mycompany/myapp:latest ``` **Copy specific platform** from a multi-arch image: ```bash hubmanager copy nginx:latest myuser/nginx-amd64:latest --platform linux/amd64 ``` --- ### `prune` — Delete outdated tags ```text hubmanager prune [options] Options: --keep N Number of recent tags to keep (default: 3) --older-than DAYS Delete tags older than N days (overrides --keep) --exclude PATTERN Extended regex of tags to never delete Default: "^(latest|stable|main|master|release)$" --no-exclude Disable the default exclusion pattern -n, --dry-run Show what would be deleted without deleting -y, --yes Skip confirmation prompt ``` Tags are sorted by image creation date (newest first). The newest N are kept; the rest are deleted. Tags matching the exclusion pattern are always preserved. ```bash # Preview: keep 5 most recent, protect default tags hubmanager prune myuser/myapp --keep 5 --dry-run # Keep 3 most recent, auto-confirm hubmanager prune myuser/myapp --keep 3 --yes # Delete anything older than 30 days hubmanager prune myuser/myapp --older-than 30 --dry-run # Custom exclusion: never delete latest or any semver tag hubmanager prune myuser/myapp --keep 5 \ --exclude "^(latest|v[0-9]+\.[0-9]+(\.[0-9]+)?)$" # Prune everything (no exclusions) hubmanager prune myuser/myapp --keep 1 --no-exclude --dry-run ``` --- ## Authentication hubmanager automatically detects the authentication method by probing the registry's `/v2/` endpoint: | Registry type | Auth method | | --- | --- | | Docker Hub | Bearer tokens via `auth.docker.io` | | Harbor, self-hosted with token server | Bearer tokens via registry-configured realm | | Basic-auth self-hosted | HTTP Basic Auth on every request | | Public/anonymous registries | No auth | Bearer tokens are cached in memory for the duration of the session and refreshed automatically when they expire. ### Docker Hub notes - `list` uses the Docker Hub REST API (`hub.docker.com`) because the `_catalog` endpoint is restricted on Docker Hub. - `delete` is not supported via the v2 API on Docker Hub. Use the web UI at . - `prune` is not supported on Docker Hub for the same reason. ### Cleaning up empty repositories The Docker Registry v2 API does not provide an endpoint to delete repositories. After deleting all tags from a repository, it will still appear in `hubmanager list`. This is a limitation of the registry spec, not of hubmanager. To remove empty repositories, you need direct access to the registry host: ```bash # 1. Delete the repository directory from storage rm -rf /var/lib/registry/docker/registry/v2/repositories/ # 2. Run garbage collection to reclaim disk space docker exec registry garbage-collect /etc/docker/registry/config.yml # 3. Restart the registry so the catalog refreshes docker restart ``` --- ## JSON output All commands support `--json` for machine-readable output: ```bash # Get all tags as a JSON array hubmanager tags myapp --json | jq '.tags' # Get digest of latest hubmanager inspect myapp:latest --json | jq '._digest' # Delete and capture result hubmanager delete myapp:old --yes --json # {"deleted":true,"digest":"sha256:..."} # Prune and capture counts hubmanager prune myapp --keep 3 --yes --json # {"deleted":5,"failed":0} ``` --- ## Exit Codes | Code | Meaning | | --- | --- | | 0 | Success | | 1 | General / usage error | | 2 | Authentication failure | | 3 | Resource not found (404) | | 4 | Permission denied (403) | | 5 | Registry server error (5xx) | | 6 | Network / connectivity error | | 7 | Operation not supported by registry | --- ## Examples ```bash # Mirror all tags of an image to a private registry for tag in $(hubmanager tags nginx --json | jq -r '.tags[]'); do hubmanager copy nginx:$tag \ --src-registry https://registry-1.docker.io \ --dst-registry https://registry.example.com \ mycompany/nginx:$tag done # List images older than 60 days (dry run) hubmanager prune myuser/myapp --older-than 60 --dry-run # Get the SHA256 digest of the production image DIGEST=$(hubmanager inspect myuser/myapp:production --json | jq -r '._digest') echo "Production image: $DIGEST" # Promote staging image to production (retag) hubmanager copy myuser/myapp:staging myuser/myapp:production ``` --- ## Configuration reference | Key | Description | | --- | --- | | `REGISTRY` | Default registry URL | | `USERNAME` | Default username | | `PASSWORD` | Default password or token; prefix with `enc:` for encrypted values | | `CACHE_TIMEOUT` | Passphrase keyring cache TTL in seconds (default: 300) | | `REGISTRY__URL` | URL for a named registry alias | | `REGISTRY__USERNAME` | Username for a named alias | | `REGISTRY__PASSWORD` | Password for a named alias (supports `enc:` prefix) | Aliases are case-insensitive and treat `-` as `_`. For example, alias `my-reg` maps to `REGISTRY_MY_REG_URL`.