You've already forked hubmanager
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 242eeca238 | |||
| e70596cd59 |
98
CLAUDE.md
98
CLAUDE.md
@@ -10,6 +10,86 @@ authentication detection (bearer token or HTTP basic auth).
|
||||
|
||||
---
|
||||
|
||||
## What Changed (Session 2026-03-01b)
|
||||
|
||||
### Passphrase caching via Linux keyring (v0.3.0)
|
||||
|
||||
Added cross-invocation master passphrase caching using the Linux kernel keyring
|
||||
(`keyctl` from `keyutils`). `keyctl` is an **optional** dependency — if not
|
||||
installed, behaviour is unchanged (prompt every time).
|
||||
|
||||
**New functions** (Encryption helpers section):
|
||||
|
||||
| Function | Purpose |
|
||||
| --- | --- |
|
||||
| `_keyring_available()` | Returns 0 if `keyctl` is installed |
|
||||
| `_keyring_get()` | Read cached passphrase from user keyring; return 1 if missing/expired |
|
||||
| `_keyring_set(pass, ttl)` | Store passphrase in keyring with TTL |
|
||||
| `_keyring_clear()` | Revoke the cached key |
|
||||
|
||||
**New subcommands**:
|
||||
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `unlock` | Prompt for passphrase and cache in keyring (useful before scripting) |
|
||||
| `lock` | Clear cached passphrase from keyring immediately |
|
||||
|
||||
**New global option**: `--cache-timeout <seconds>` (default 300 / 5 min).
|
||||
Also configurable via `CACHE_TIMEOUT=<seconds>` in `hubmanager.conf`.
|
||||
|
||||
**New constants**: `HM_KEYRING_KEY`, `HM_KEYRING_DEFAULT_TIMEOUT`.
|
||||
**New global variable**: `HM_CACHE_TIMEOUT`.
|
||||
|
||||
**Modified functions**:
|
||||
|
||||
- `_prompt_master_pass()` — checks keyring before prompting; stores after prompt
|
||||
- `_prompt_set_master_pass()` — stores in keyring after confirmation
|
||||
|
||||
**Version bump**: `0.2.1` → `0.3.0`
|
||||
|
||||
---
|
||||
|
||||
## What Changed (Session 2026-03-01)
|
||||
|
||||
### Bug fixes and improvements (v0.2.1)
|
||||
|
||||
1. **Global arg parsing fix** (`parse_global_args`): global flags like `--user`,
|
||||
`--registry`, `--password` placed after the subcommand name (e.g.
|
||||
`hubmanager login --user ...`) were not recognised. The `*` catch-all used
|
||||
`remaining+=("$@"); break` which stopped parsing. Changed to
|
||||
`remaining+=("$1"); shift` so parsing continues through all arguments.
|
||||
|
||||
2. **Subshell variable fix** (`raw_http` / `registry_request`): `HM_LAST_HTTP_CODE`
|
||||
and `HM_LAST_HEADERS_FILE` were set inside `raw_http`, but callers used
|
||||
`body=$(raw_http ...)` which runs in a subshell — variables set inside never
|
||||
propagated back. Fixed by adding two global temp files (`HM_HTTP_CODE_FILE`,
|
||||
`HM_HEADERS_REF_FILE`) that `raw_http` writes to; callers read them back after
|
||||
the subshell returns.
|
||||
|
||||
3. **Config inline-comment stripping removed** (`load_config`): the parser stripped
|
||||
everything after the first `#` in config values (`value="${value%%#*}"`), which
|
||||
could truncate base64 ciphertext containing `#`. Removed since comment-only
|
||||
lines are already skipped.
|
||||
|
||||
4. **Decrypt newline fix** (`_decrypt_value`): `printf '%s'` piped ciphertext to
|
||||
`openssl enc -d -a` without a trailing newline, causing `openssl` to fail with
|
||||
"error reading input file". Changed to `printf '%s\n'`.
|
||||
|
||||
5. **Config path moved**: `~/.hubmanager.conf` → `~/.config/hubmanager.conf`.
|
||||
`login --save` now runs `mkdir -p` on the parent directory before writing.
|
||||
|
||||
6. **README**: added "Cleaning up empty repositories" section explaining the
|
||||
server-side steps needed to remove empty repos (registry v2 API limitation).
|
||||
|
||||
### New global variables
|
||||
|
||||
| Variable | Purpose |
|
||||
| --- | --- |
|
||||
| `HM_HTTP_CODE_FILE` | Temp file for HTTP code (survives subshells) |
|
||||
| `HM_HEADERS_REF_FILE` | Temp file for headers path (survives subshells) |
|
||||
|
||||
---
|
||||
|
||||
## What Changed (Session 2026-02-21)
|
||||
|
||||
### Encrypted config values (`--encrypt`)
|
||||
@@ -57,13 +137,15 @@ Both `load_config()` and `resolve_registry_alias()` detect the prefix and call
|
||||
|
||||
| Command | Description |
|
||||
| --- | --- |
|
||||
| `login` | Test credentials against a registry; `--save` writes to `~/.hubmanager.conf` |
|
||||
| `login` | Test credentials against a registry; `--save` writes to `~/.config/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 |
|
||||
| `unlock` | Cache master passphrase in kernel keyring (requires `keyctl`) |
|
||||
| `lock` | Clear cached master passphrase from keyring |
|
||||
|
||||
### Supporting files
|
||||
|
||||
@@ -85,11 +167,12 @@ set -euo pipefail
|
||||
# --- Output / Formatting helpers ---
|
||||
# --- Dependency check ---
|
||||
# --- Encryption helpers --- _encrypt_value(), _decrypt_value(), _prompt_master_pass()
|
||||
# _keyring_get(), _keyring_set(), _keyring_clear()
|
||||
# --- 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
|
||||
# --- Subcommands --- cmd_login/unlock/lock/list/tags/inspect/delete/copy/prune
|
||||
# --- Global arg parsing --- parse_global_args()
|
||||
# --- Main dispatcher --- main()
|
||||
main "$@"
|
||||
@@ -120,7 +203,7 @@ main "$@"
|
||||
- `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`):
|
||||
6. **Config file** (`~/.config/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
|
||||
@@ -143,6 +226,14 @@ main "$@"
|
||||
`HM_MASTER_PASS`).
|
||||
- `openssl` is an optional dependency: not checked at startup, only on first `enc:` encounter.
|
||||
|
||||
10. **Passphrase caching** (v0.3.0):
|
||||
- Uses Linux kernel keyring (`keyctl` from `keyutils`) to cache the master passphrase
|
||||
across invocations with a configurable TTL (default 300s / 5 min).
|
||||
- `_prompt_master_pass()` checks keyring before prompting; stores after prompt.
|
||||
- `keyctl` is optional: if not installed, each invocation prompts as before (no errors).
|
||||
- `unlock` pre-caches the passphrase; `lock` clears it immediately.
|
||||
- TTL configurable via `--cache-timeout <seconds>` flag or `CACHE_TIMEOUT` config key.
|
||||
|
||||
---
|
||||
|
||||
## Global Variables (key ones)
|
||||
@@ -158,6 +249,7 @@ main "$@"
|
||||
| `HM_TOKEN_CACHE["registry\|scope"]` | Cached bearer token |
|
||||
| `HM_TOKEN_EXPIRY["registry\|scope"]` | Token expiry (epoch seconds) |
|
||||
| `HM_MASTER_PASS` | Master passphrase for `enc:` config values (session-cached) |
|
||||
| `HM_CACHE_TIMEOUT` | Keyring cache TTL in seconds (default 300) |
|
||||
| `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` |
|
||||
|
||||
97
README.md
97
README.md
@@ -8,6 +8,7 @@ A Bash CLI tool to manage Docker Registry images remotely. Supports Docker Hub a
|
||||
- **curl**
|
||||
- **jq**
|
||||
- **openssl** *(optional — required only when using encrypted config values)*
|
||||
- **keyctl** *(optional — from `keyutils`; enables passphrase caching across invocations)*
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -48,12 +49,12 @@ hubmanager prune myuser/myapp --keep 3 --dry-run
|
||||
|
||||
## Configuration
|
||||
|
||||
Credentials and registry settings are stored in `~/.hubmanager.conf`.
|
||||
Credentials and registry settings are stored in `~/.config/hubmanager.conf`.
|
||||
The file uses a simple `KEY=VALUE` format:
|
||||
|
||||
```bash
|
||||
# ~/.hubmanager.conf
|
||||
# chmod 600 ~/.hubmanager.conf
|
||||
# ~/.config/hubmanager.conf
|
||||
# chmod 600 ~/.config/hubmanager.conf
|
||||
|
||||
# Default registry and credentials
|
||||
REGISTRY=https://registry.example.com
|
||||
@@ -91,7 +92,7 @@ hubmanager login --registry https://registry.example.com \
|
||||
# New master passphrase: ****
|
||||
# Confirm master passphrase: ****
|
||||
# Login Succeeded — bearer auth, registry: https://registry.example.com
|
||||
# Credentials saved to /home/user/.hubmanager.conf
|
||||
# Credentials saved to /home/user/.config/hubmanager.conf
|
||||
# Password stored encrypted (AES-256-CBC). Master passphrase required on each use.
|
||||
```
|
||||
|
||||
@@ -107,6 +108,33 @@ The `enc:` prefix also works for named alias passwords (`REGISTRY_<ALIAS>_PASSWO
|
||||
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
|
||||
@@ -116,7 +144,8 @@ hubmanager [OPTIONS] <command> [COMMAND OPTIONS]
|
||||
Default: https://registry-1.docker.io
|
||||
-u, --user <username> Username (overrides config file)
|
||||
-p, --password <pass> Password or token (overrides config file)
|
||||
--config <file> Config file path (default: ~/.hubmanager.conf)
|
||||
--config <file> Config file path (default: ~/.config/hubmanager.conf)
|
||||
--cache-timeout <s> 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)
|
||||
@@ -142,7 +171,43 @@ Add `--encrypt` to store the password encrypted with AES-256-CBC (requires `open
|
||||
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/.hubmanager.conf
|
||||
# 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.
|
||||
```
|
||||
|
||||
---
|
||||
@@ -353,6 +418,25 @@ Bearer tokens are cached in memory for the duration of the session and refreshed
|
||||
- `delete` is not supported via the v2 API on Docker Hub. Use the web UI at <https://hub.docker.com>.
|
||||
- `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/<repo-name>
|
||||
|
||||
# 2. Run garbage collection to reclaim disk space
|
||||
docker exec <registry-container> registry garbage-collect /etc/docker/registry/config.yml
|
||||
|
||||
# 3. Restart the registry so the catalog refreshes
|
||||
docker restart <registry-container>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JSON output
|
||||
@@ -423,6 +507,7 @@ hubmanager copy myuser/myapp:staging myuser/myapp:production
|
||||
| `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_<ALIAS>_URL` | URL for a named registry alias |
|
||||
| `REGISTRY_<ALIAS>_USERNAME` | Username for a named alias |
|
||||
| `REGISTRY_<ALIAS>_PASSWORD` | Password for a named alias (supports `enc:` prefix) |
|
||||
|
||||
136
hubmanager
136
hubmanager
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# hubmanager - Manage Docker Registry images remotely
|
||||
# Version: 0.2.0
|
||||
# Version: 0.3.0
|
||||
# Dependencies: curl, jq, bash 4+
|
||||
# Usage: hubmanager --help
|
||||
|
||||
@@ -9,10 +9,12 @@ set -euo pipefail
|
||||
# =============================================================================
|
||||
# --- Constants ---
|
||||
# =============================================================================
|
||||
readonly HM_VERSION="0.2.0"
|
||||
readonly HM_VERSION="0.3.0"
|
||||
readonly HM_DEFAULT_REGISTRY="https://registry-1.docker.io"
|
||||
readonly HM_DEFAULT_CONFIG="${HOME}/.hubmanager.conf"
|
||||
readonly HM_DEFAULT_CONFIG="${HOME}/.config/hubmanager.conf"
|
||||
readonly HM_DOCKERHUB_API="https://hub.docker.com"
|
||||
readonly HM_KEYRING_KEY="hubmanager:master"
|
||||
readonly HM_KEYRING_DEFAULT_TIMEOUT=300 # seconds (5 minutes)
|
||||
|
||||
# =============================================================================
|
||||
# --- Global state ---
|
||||
@@ -35,12 +37,15 @@ declare -A HM_CONFIG_VARS=() # raw config key/value pairs
|
||||
|
||||
HM_HUB_TOKEN="" # Docker Hub REST API token (JWT)
|
||||
HM_MASTER_PASS="" # Master passphrase for encrypted config values (session-scoped)
|
||||
HM_CACHE_TIMEOUT="$HM_KEYRING_DEFAULT_TIMEOUT" # Keyring cache TTL in seconds
|
||||
HM_TMPFILES=()
|
||||
HM_LAST_HTTP_CODE=""
|
||||
HM_LAST_HEADERS_FILE=""
|
||||
HM_HTTP_CODE_FILE=$(mktemp)
|
||||
HM_HEADERS_REF_FILE=$(mktemp)
|
||||
|
||||
trap '_hm_cleanup' EXIT INT TERM
|
||||
_hm_cleanup() { rm -f "${HM_TMPFILES[@]}" 2>/dev/null || true; }
|
||||
_hm_cleanup() { rm -f "${HM_TMPFILES[@]}" "$HM_HTTP_CODE_FILE" "$HM_HEADERS_REF_FILE" 2>/dev/null || true; }
|
||||
|
||||
# =============================================================================
|
||||
# --- Output / Formatting helpers ---
|
||||
@@ -109,14 +114,54 @@ _require_openssl() {
|
||||
die "openssl is required for encrypted config values. Install openssl and retry."
|
||||
}
|
||||
|
||||
# Prompt once per session; result cached in HM_MASTER_PASS
|
||||
# --- Keyring helpers (optional; keyctl from keyutils) ---
|
||||
|
||||
# Check if keyctl is available
|
||||
_keyring_available() { command -v keyctl &>/dev/null; }
|
||||
|
||||
# Try to read the cached passphrase from the user session keyring.
|
||||
# Outputs the passphrase on stdout; returns 1 if not found or expired.
|
||||
_keyring_get() {
|
||||
_keyring_available || return 1
|
||||
local key_id
|
||||
key_id=$(keyctl request user "$HM_KEYRING_KEY" 2>/dev/null) || return 1
|
||||
keyctl pipe "$key_id" 2>/dev/null || return 1
|
||||
}
|
||||
|
||||
# Store passphrase in the user session keyring with a TTL.
|
||||
# Args: <passphrase> [timeout_seconds]
|
||||
_keyring_set() {
|
||||
_keyring_available || return 0
|
||||
local pass="$1"
|
||||
local ttl="${2:-$HM_CACHE_TIMEOUT}"
|
||||
local key_id
|
||||
key_id=$(keyctl add user "$HM_KEYRING_KEY" "$pass" @u 2>/dev/null) || return 0
|
||||
keyctl timeout "$key_id" "$ttl" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Revoke (clear) the cached passphrase from the keyring.
|
||||
_keyring_clear() {
|
||||
_keyring_available || return 0
|
||||
local key_id
|
||||
key_id=$(keyctl request user "$HM_KEYRING_KEY" 2>/dev/null) || return 0
|
||||
keyctl revoke "$key_id" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Prompt once per session; result cached in HM_MASTER_PASS and keyring
|
||||
_prompt_master_pass() {
|
||||
[[ -n "$HM_MASTER_PASS" ]] && return 0
|
||||
# Try keyring cache first
|
||||
local cached
|
||||
if cached=$(_keyring_get); then
|
||||
HM_MASTER_PASS="$cached"
|
||||
return 0
|
||||
fi
|
||||
_require_openssl
|
||||
printf "hubmanager master passphrase: " >/dev/tty
|
||||
read -rs HM_MASTER_PASS </dev/tty
|
||||
printf "\n" >/dev/tty
|
||||
[[ -n "$HM_MASTER_PASS" ]] || die "Master passphrase cannot be empty."
|
||||
_keyring_set "$HM_MASTER_PASS" "$HM_CACHE_TIMEOUT"
|
||||
}
|
||||
|
||||
# Prompt and confirm a new master passphrase (used by login --encrypt)
|
||||
@@ -130,6 +175,7 @@ _prompt_set_master_pass() {
|
||||
[[ "$pass1" == "$pass2" ]] || die "Passphrases do not match."
|
||||
[[ -n "$pass1" ]] || die "Master passphrase cannot be empty."
|
||||
HM_MASTER_PASS="$pass1"
|
||||
_keyring_set "$HM_MASTER_PASS" "$HM_CACHE_TIMEOUT"
|
||||
}
|
||||
|
||||
# Encrypt PLAINTEXT using AES-256-CBC; outputs base64 ciphertext (no newlines)
|
||||
@@ -152,7 +198,7 @@ _decrypt_value() {
|
||||
local pass_file result
|
||||
pass_file=$(mktemp); HM_TMPFILES+=("$pass_file")
|
||||
printf '%s' "$HM_MASTER_PASS" > "$pass_file"
|
||||
if ! result=$(printf '%s' "$ciphertext" | \
|
||||
if ! result=$(printf '%s\n' "$ciphertext" | \
|
||||
openssl enc -d -aes-256-cbc -pbkdf2 -a -pass "file:${pass_file}" 2>/dev/null); then
|
||||
die "Failed to decrypt config value. Wrong master passphrase?"
|
||||
fi
|
||||
@@ -188,8 +234,7 @@ load_config() {
|
||||
key="${key%"${key##*[![:space:]]}"}"
|
||||
[[ -z "$key" ]] && continue
|
||||
|
||||
# Strip inline comments and trim whitespace from value
|
||||
value="${value%%#*}"
|
||||
# Trim whitespace from value
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
|
||||
@@ -207,6 +252,10 @@ load_config() {
|
||||
HM_PASSWORD="$pw"
|
||||
fi
|
||||
fi
|
||||
# Cache timeout (only override if not set via CLI)
|
||||
if [[ "$HM_CACHE_TIMEOUT" == "$HM_KEYRING_DEFAULT_TIMEOUT" && -v 'HM_CONFIG_VARS[CACHE_TIMEOUT]' ]]; then
|
||||
HM_CACHE_TIMEOUT="${HM_CONFIG_VARS[CACHE_TIMEOUT]}"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_registry_alias() {
|
||||
@@ -275,6 +324,10 @@ raw_http() {
|
||||
"$url" \
|
||||
2>/dev/null) || HM_LAST_HTTP_CODE="000"
|
||||
|
||||
# Persist code and headers path to files so they survive subshells
|
||||
printf '%s' "$HM_LAST_HTTP_CODE" > "$HM_HTTP_CODE_FILE"
|
||||
printf '%s' "$header_file" > "$HM_HEADERS_REF_FILE"
|
||||
|
||||
verbose "<<< HTTP $HM_LAST_HTTP_CODE"
|
||||
|
||||
[[ "$method" != "HEAD" ]] && cat "$body_file"
|
||||
@@ -469,6 +522,8 @@ registry_request() {
|
||||
|
||||
local body
|
||||
body=$(raw_http "$method" "$url" "${auth_args[@]}" "${headers[@]}" "${extra[@]}")
|
||||
HM_LAST_HTTP_CODE=$(<"$HM_HTTP_CODE_FILE")
|
||||
HM_LAST_HEADERS_FILE=$(<"$HM_HEADERS_REF_FILE")
|
||||
|
||||
# On 401: refresh token using the challenge scope and retry once
|
||||
if [[ "$HM_LAST_HTTP_CODE" == "401" ]]; then
|
||||
@@ -489,6 +544,8 @@ registry_request() {
|
||||
[[ -n "$retry_auth" ]] && retry_auth_args=("-H" "$retry_auth")
|
||||
|
||||
body=$(raw_http "$method" "$url" "${retry_auth_args[@]}" "${headers[@]}" "${extra[@]}")
|
||||
HM_LAST_HTTP_CODE=$(<"$HM_HTTP_CODE_FILE")
|
||||
HM_LAST_HEADERS_FILE=$(<"$HM_HEADERS_REF_FILE")
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -597,7 +654,8 @@ ${C_BOLD}Options:${C_RESET}
|
||||
-r, --registry <url> Registry URL (default: https://registry-1.docker.io)
|
||||
-u, --user <username> Username
|
||||
-p, --password <pass> Password or token
|
||||
--config <file> Config file (default: ~/.hubmanager.conf)
|
||||
--config <file> Config file (default: ~/.config/hubmanager.conf)
|
||||
--cache-timeout <s> Passphrase cache TTL in seconds (default: 300)
|
||||
--json Output raw JSON
|
||||
--no-color Disable color output
|
||||
-v, --verbose Verbose mode
|
||||
@@ -613,6 +671,8 @@ ${C_BOLD}Commands:${C_RESET}
|
||||
copy <src> <dst> Copy/retag an image
|
||||
prune <image> Delete outdated tags for an image
|
||||
login Test credentials and optionally save to config (--save [--encrypt])
|
||||
unlock Cache master passphrase in kernel keyring (requires keyctl)
|
||||
lock Clear cached master passphrase
|
||||
|
||||
Run ${C_BOLD}hubmanager <command> --help${C_RESET} for command-specific options.
|
||||
EOF
|
||||
@@ -625,6 +685,50 @@ cmd_help() { show_usage; }
|
||||
# =============================================================================
|
||||
cmd_version() { echo "hubmanager $HM_VERSION"; }
|
||||
|
||||
# =============================================================================
|
||||
# --- Subcommand: unlock ---
|
||||
# =============================================================================
|
||||
cmd_unlock() {
|
||||
if [[ "${1:-}" == "--help" ]]; then
|
||||
cat <<EOF
|
||||
${C_BOLD}Usage:${C_RESET} hubmanager unlock [--timeout <seconds>]
|
||||
|
||||
Cache the master passphrase in the Linux kernel keyring so subsequent
|
||||
commands do not re-prompt. Requires ${C_BOLD}keyctl${C_RESET} (keyutils package).
|
||||
|
||||
--timeout <sec> Cache duration (default: ${HM_KEYRING_DEFAULT_TIMEOUT}s / 5 min)
|
||||
EOF
|
||||
return 0
|
||||
fi
|
||||
_keyring_available || die "keyctl (keyutils) is required for passphrase caching. Install keyutils and retry."
|
||||
_require_openssl
|
||||
# Force a fresh prompt even if already cached
|
||||
HM_MASTER_PASS=""
|
||||
printf "hubmanager master passphrase: " >/dev/tty
|
||||
read -rs HM_MASTER_PASS </dev/tty
|
||||
printf "\n" >/dev/tty
|
||||
[[ -n "$HM_MASTER_PASS" ]] || die "Master passphrase cannot be empty."
|
||||
_keyring_set "$HM_MASTER_PASS" "$HM_CACHE_TIMEOUT"
|
||||
info "Passphrase cached for ${HM_CACHE_TIMEOUT}s."
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# --- Subcommand: lock ---
|
||||
# =============================================================================
|
||||
cmd_lock() {
|
||||
if [[ "${1:-}" == "--help" ]]; then
|
||||
cat <<EOF
|
||||
${C_BOLD}Usage:${C_RESET} hubmanager lock
|
||||
|
||||
Clear the cached master passphrase from the Linux kernel keyring.
|
||||
EOF
|
||||
return 0
|
||||
fi
|
||||
_keyring_clear
|
||||
HM_MASTER_PASS=""
|
||||
info "Passphrase cache cleared."
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# --- Subcommand: login ---
|
||||
# =============================================================================
|
||||
@@ -643,7 +747,7 @@ Usage: hubmanager login [--registry URL] [--user USER] [--password PASS] [--save
|
||||
Test credentials against the registry.
|
||||
|
||||
Options:
|
||||
--save Write validated credentials to the config file (~/.hubmanager.conf)
|
||||
--save Write validated credentials to the config file (~/.config/hubmanager.conf)
|
||||
--encrypt Encrypt the password in the config file with AES-256-CBC.
|
||||
A master passphrase will be prompted; you must enter the same
|
||||
passphrase on every subsequent hubmanager invocation that reads
|
||||
@@ -682,6 +786,7 @@ EOF
|
||||
|
||||
if [[ "$do_save" == true ]]; then
|
||||
local config="$HM_CONFIG_FILE"
|
||||
mkdir -p "$(dirname "$config")"
|
||||
local pw_entry=""
|
||||
if [[ -n "$HM_PASSWORD" ]]; then
|
||||
if [[ "$do_encrypt" == true ]]; then
|
||||
@@ -1187,6 +1292,8 @@ EOF
|
||||
local manifest_body
|
||||
manifest_body=$(raw_http GET "${src_registry}/v2/${src_image}/manifests/${src_tag}" \
|
||||
"${src_auth[@]}" "${MANIFEST_ACCEPT_HEADERS[@]}")
|
||||
HM_LAST_HTTP_CODE=$(<"$HM_HTTP_CODE_FILE")
|
||||
HM_LAST_HEADERS_FILE=$(<"$HM_HEADERS_REF_FILE")
|
||||
[[ "$HM_LAST_HTTP_CODE" == "200" ]] || die "Failed to fetch source manifest (HTTP $HM_LAST_HTTP_CODE)"
|
||||
|
||||
local content_type
|
||||
@@ -1208,6 +1315,8 @@ EOF
|
||||
|
||||
manifest_body=$(raw_http GET "${src_registry}/v2/${src_image}/manifests/${plat_digest}" \
|
||||
"${src_auth[@]}" "${MANIFEST_ACCEPT_HEADERS[@]}")
|
||||
HM_LAST_HTTP_CODE=$(<"$HM_HTTP_CODE_FILE")
|
||||
HM_LAST_HEADERS_FILE=$(<"$HM_HEADERS_REF_FILE")
|
||||
[[ "$HM_LAST_HTTP_CODE" == "200" ]] || die "Failed to fetch platform manifest (HTTP $HM_LAST_HTTP_CODE)"
|
||||
content_type=$(get_response_header "content-type") || true
|
||||
fi
|
||||
@@ -1580,6 +1689,7 @@ parse_global_args() {
|
||||
-u|--user) HM_USERNAME="$2"; shift 2 ;;
|
||||
-p|--password) HM_PASSWORD="$2"; shift 2 ;;
|
||||
--config) HM_CONFIG_FILE="$2"; shift 2 ;;
|
||||
--cache-timeout) HM_CACHE_TIMEOUT="$2"; shift 2 ;;
|
||||
--json) HM_OPT_JSON=true; shift ;;
|
||||
--no-color) HM_OPT_NO_COLOR=true; shift ;;
|
||||
-v|--verbose) HM_OPT_VERBOSE=true; shift ;;
|
||||
@@ -1587,8 +1697,8 @@ parse_global_args() {
|
||||
-h|--help) show_usage; exit 0 ;;
|
||||
--version) cmd_version; exit 0 ;;
|
||||
--) shift; remaining+=("$@"); break ;;
|
||||
# Stop global parsing at first non-flag arg or unknown flag
|
||||
*) remaining+=("$@"); break ;;
|
||||
# Pass non-global args through, but keep parsing remaining args
|
||||
*) remaining+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -1621,6 +1731,8 @@ main() {
|
||||
copy) cmd_copy "$@" ;;
|
||||
prune) cmd_prune "$@" ;;
|
||||
login) cmd_login "$@" ;;
|
||||
unlock) cmd_unlock "$@" ;;
|
||||
lock) cmd_lock "$@" ;;
|
||||
help|-h|--help) cmd_help ;;
|
||||
version|--version) cmd_version ;;
|
||||
*)
|
||||
|
||||
Reference in New Issue
Block a user