Template
Replaces the --env-file ~/.creds/*.env pattern with Bitwarden-sourced
credentials. Each agent identity stores its own API keys in a BW
collection; bw-run.sh fetches them at runtime and passes to CLI
containers via a temp env file (cleaned up on exit).
Usage: bw-run.sh <agent> <service> <image> [args...]
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
158 lines
5.3 KiB
Bash
Executable File
158 lines
5.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# bw-run.sh — run a CLI command with credentials sourced from Bitwarden.
|
|
#
|
|
# Replaces the --env-file ~/.creds/<service>.env pattern. Each agent identity
|
|
# stores its own API keys/passwords in a Bitwarden collection. This script
|
|
# fetches the credential, writes it to a temp env file, runs the CLI, and
|
|
# cleans up — no credential material persists on disk.
|
|
#
|
|
# Usage:
|
|
# bw-run.sh <agent> <service> <image> [args...]
|
|
#
|
|
# Examples:
|
|
# bw-run.sh vp-techops redmine git.knownelement.com/reachableceo/redmine-cli:latest list --assigned-to-me -p 55
|
|
# bw-run.sh vp-techops discourse git.knownelement.com/reachableceo/discourse-cli:latest ls -c vp-techops
|
|
# bw-run.sh reachableceo redmine git.knownelement.com/reachableceo/redmine-cli:latest whoami
|
|
#
|
|
# Prerequisites:
|
|
# - BW_CLIENTID and BW_CLIENTSECRET exported (or in ~/.config/bw/env)
|
|
# - Or BW_SESSION exported from a prior `bw unlock`
|
|
#
|
|
# Bitwarden item convention:
|
|
# Collection: <agent-name> (or "shared" for cross-agent creds)
|
|
# Item name: "<agent-name> <Service>" (e.g., "vp-techops Redmine")
|
|
# Fields: URL, USERNAME, PASSWORD, API_KEY, and custom fields as needed
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
# shellcheck disable=SC1091
|
|
source "$HERE/lib/common.sh"
|
|
|
|
###############################################################################
|
|
|
|
print_usage() {
|
|
sed -n '2,26p' "$0"
|
|
exit 0
|
|
}
|
|
|
|
if [ $# -lt 3 ] || [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
|
|
print_usage
|
|
fi
|
|
|
|
AGENT="$1"
|
|
SERVICE="$2"
|
|
IMAGE="$3"
|
|
shift 3
|
|
|
|
ITEM_NAME="${AGENT} ${SERVICE}"
|
|
|
|
###############################################################################
|
|
# Ensure Bitwarden session is active
|
|
###############################################################################
|
|
|
|
ensure_bw_session() {
|
|
# If BW_SESSION is already set, trust it
|
|
if [ -n "${BW_SESSION:-}" ]; then
|
|
return 0
|
|
fi
|
|
|
|
# Try machine-to-machine auth via client credentials
|
|
if [ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ]; then
|
|
log_info "Authenticating to Bitwarden via API key..."
|
|
if bw login --apikey >/dev/null 2>&1; then
|
|
export BW_SESSION
|
|
BW_SESSION=$(bw unlock --raw 2>/dev/null || true)
|
|
if [ -z "$BW_SESSION" ]; then
|
|
die "Bitwarden unlock failed after API-key login."
|
|
fi
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# Try reading from the bw env file
|
|
local bw_env="${HOME}/.config/bw/env"
|
|
if [ -f "$bw_env" ]; then
|
|
# shellcheck disable=SC1090
|
|
. "$bw_env"
|
|
if [ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ]; then
|
|
log_info "Authenticating to Bitwarden via env file..."
|
|
bw login --apikey >/dev/null 2>&1 || true
|
|
export BW_SESSION
|
|
BW_SESSION=$(bw unlock --raw 2>/dev/null || true)
|
|
if [ -n "$BW_SESSION" ]; then
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
die "No Bitwarden session. Set BW_SESSION or BW_CLIENTID/BW_CLIENTSECRET."
|
|
}
|
|
|
|
###############################################################################
|
|
|
|
ensure_bw_session
|
|
|
|
log_info "Fetching credential: ${ITEM_NAME}"
|
|
|
|
# Fetch the Bitwarden item and extract fields into env vars
|
|
RAW_JSON=$(bw get item "$ITEM_NAME" 2>/dev/null) || die "Bitwarden item not found: ${ITEM_NAME}"
|
|
|
|
# Build env file content from the JSON response
|
|
ENV_CONTENT=$(echo "$RAW_JSON" | jq -r '
|
|
def safename(f): f | gsub("[^A-Za-z0-9_]"; "_") | ascii_upcase;
|
|
.login as $login |
|
|
# Standard fields
|
|
(
|
|
if $login.uris[0].uri then "URL=\($login.uris[0].uri)\n" else "" end
|
|
) +
|
|
(
|
|
if $login.username then "USERNAME=\($login.username)\n" else "" end
|
|
) +
|
|
(
|
|
if $login.password then "PASSWORD=\($login.password)\n" else "" end
|
|
) +
|
|
# Map known field names to common env var patterns
|
|
(
|
|
.fields[]? |
|
|
if .name == "API_KEY" or .name == "api_key" or .name == "apikey" then
|
|
"API_KEY=\(.value)\n"
|
|
elif .name == "token" or .name == "TOKEN" then
|
|
"TOKEN=\(.value)\n"
|
|
else
|
|
"\(safename(.name))=\(.value)\n"
|
|
end
|
|
) +
|
|
# Also emit REDMINE_API_KEY / DISCOURSE_API_KEY etc. from the password field
|
|
# (many CLI tools expect the key in the password field)
|
|
""
|
|
') || die "Failed to parse Bitwarden item JSON."
|
|
|
|
if [ -z "$ENV_CONTENT" ]; then
|
|
die "No credential fields found in Bitwarden item: ${ITEM_NAME}"
|
|
fi
|
|
|
|
# Also check if the password field IS the API key (common for CLI creds)
|
|
# and add a service-specific key env var based on the service name
|
|
SERVICE_UPPER=$(echo "$SERVICE" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
|
|
API_KEY_VAR="${SERVICE_UPPER}_API_KEY"
|
|
|
|
# If the service-specific key var isn't already in ENV_CONTENT, try adding it from password
|
|
if ! echo "$ENV_CONTENT" | grep -q "^${API_KEY_VAR}="; then
|
|
PASSWORD_VAL=$(echo "$RAW_JSON" | jq -r '.login.password // empty')
|
|
if [ -n "$PASSWORD_VAL" ]; then
|
|
ENV_CONTENT="${API_KEY_VAR}=${PASSWORD_VAL}
|
|
${ENV_CONTENT}"
|
|
fi
|
|
fi
|
|
|
|
# Write to temp file, cleaned up on exit
|
|
ENVFILE=$(mktemp "/tmp/bw-${AGENT}-${SERVICE}-XXXXXX.env")
|
|
trap 'rm -f "$ENVFILE"' EXIT
|
|
chmod 600 "$ENVFILE"
|
|
printf '%s' "$ENV_CONTENT" > "$ENVFILE"
|
|
|
|
log_ok "Credential sourced. Running: ${IMAGE} $*"
|
|
|
|
# Run the CLI with BW-sourced credentials
|
|
docker run --rm --env-file "$ENVFILE" "$IMAGE" "$@"
|