112 lines
3.8 KiB
Bash
Executable file
112 lines
3.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
CONFIG=/etc/chart-debug.conf
|
|
MAX_LOG_LINES=4000
|
|
|
|
if [[ ! -r "$CONFIG" ]]; then
|
|
echo "chart-debug is not configured" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Root-owned configuration written by install-chart-debug.
|
|
# shellcheck source=/dev/null
|
|
source "$CONFIG"
|
|
|
|
original=${1:-${SSH_ORIGINAL_COMMAND:-}}
|
|
if [[ -z "$original" || "$original" == *$'\n'* || "$original" == *$'\r'* ]]; then
|
|
echo "usage: logs --since 20m | status | container-state | recent-deploy | capture-read ID | capture-delete ID" >&2
|
|
exit 2
|
|
fi
|
|
read -r -a args <<< "$original"
|
|
|
|
logger -t chart-debug -- "user=${SUDO_USER:-${USER:-unknown}} from=${SSH_CONNECTION:-local} command=$original"
|
|
|
|
resolve_container() {
|
|
local id name
|
|
local -a matches=()
|
|
while read -r id name; do
|
|
[[ -n "$id" && "$name" =~ $CHART_CONTAINER_PATTERN ]] && matches+=("$id")
|
|
done < <(docker ps --format '{{.ID}} {{.Names}}')
|
|
if [[ ${#matches[@]} -ne 1 ]]; then
|
|
echo "container selector matched ${#matches[@]} running containers" >&2
|
|
exit 3
|
|
fi
|
|
printf '%s' "${matches[0]}"
|
|
}
|
|
|
|
redact() {
|
|
sed -E \
|
|
-e 's/(Authorization:[[:space:]]*Bearer[[:space:]]+)[A-Za-z0-9._-]+/\1[REDACTED]/Ig' \
|
|
-e 's/((token|secret|password|api[_-]?key)["=:[:space:]]+)[^,[:space:]"}]+/\1[REDACTED]/Ig' \
|
|
-e 's/([?&](code|session|state)=)[^&[:space:]]+/\1[REDACTED]/Ig'
|
|
}
|
|
|
|
valid_capture_id() {
|
|
[[ "$1" =~ ^c-[A-Za-z0-9_-]{12}$ ]]
|
|
}
|
|
|
|
case "${args[0]}" in
|
|
logs)
|
|
[[ ${#args[@]} -eq 3 && "${args[1]}" == "--since" ]] || {
|
|
echo "usage: logs --since 20m" >&2; exit 2;
|
|
}
|
|
[[ "${args[2]}" =~ ^[1-9][0-9]*(s|m|h|d)$ ]] || {
|
|
echo "invalid duration" >&2; exit 2;
|
|
}
|
|
container=$(resolve_container)
|
|
docker logs --timestamps --tail "$MAX_LOG_LINES" --since "${args[2]}" "$container" 2>&1 | redact
|
|
;;
|
|
status)
|
|
[[ ${#args[@]} -eq 1 ]] || { echo "usage: status" >&2; exit 2; }
|
|
container=$(resolve_container)
|
|
token=""
|
|
while IFS= read -r entry; do
|
|
[[ "$entry" == CHART_AUTH_TOKEN=* ]] && token=${entry#CHART_AUTH_TOKEN=}
|
|
done < <(docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$container")
|
|
if [[ -n "$token" ]]; then
|
|
[[ "$token" != *$'\n'* && "$token" != *$'\r'* ]] || {
|
|
echo "invalid CHART_AUTH_TOKEN" >&2; exit 4;
|
|
}
|
|
escaped_token=${token//\\/\\\\}
|
|
escaped_token=${escaped_token//\"/\\\"}
|
|
printf 'url = "%s/api/status"\nheader = "X-Chart-Token: %s"\nfail\nsilent\nshow-error\n' \
|
|
"$CHART_PUBLIC_URL" "$escaped_token" | curl --config - | redact
|
|
else
|
|
echo "CHART_AUTH_TOKEN is unavailable" >&2
|
|
exit 4
|
|
fi
|
|
;;
|
|
container-state)
|
|
[[ ${#args[@]} -eq 1 ]] || { echo "usage: container-state" >&2; exit 2; }
|
|
container=$(resolve_container)
|
|
docker inspect --format \
|
|
'name={{.Name}} image={{.Config.Image}} status={{.State.Status}} started={{.State.StartedAt}} restarts={{.RestartCount}} oom={{.State.OOMKilled}} exit={{.State.ExitCode}}' \
|
|
"$container"
|
|
;;
|
|
recent-deploy)
|
|
[[ ${#args[@]} -eq 1 ]] || { echo "usage: recent-deploy" >&2; exit 2; }
|
|
curl -fsS "$CHART_PUBLIC_URL/api/version"
|
|
printf '\n'
|
|
container=$(resolve_container)
|
|
docker inspect --format 'container_started={{.State.StartedAt}} image={{.Config.Image}}' "$container"
|
|
;;
|
|
capture-read)
|
|
[[ ${#args[@]} -eq 2 ]] && valid_capture_id "${args[1]}" || {
|
|
echo "usage: capture-read c-XXXXXXXXXXXX" >&2; exit 2;
|
|
}
|
|
container=$(resolve_container)
|
|
docker exec "$container" base64 "/tmp/chart-captures/${args[1]}.png"
|
|
;;
|
|
capture-delete)
|
|
[[ ${#args[@]} -eq 2 ]] && valid_capture_id "${args[1]}" || {
|
|
echo "usage: capture-delete c-XXXXXXXXXXXX" >&2; exit 2;
|
|
}
|
|
curl -fsS -X DELETE "$CHART_PUBLIC_URL/api/debug/captures/${args[1]}" >/dev/null
|
|
echo "deleted ${args[1]}"
|
|
;;
|
|
*)
|
|
echo "command not allowed" >&2
|
|
exit 2
|
|
;;
|
|
esac
|