#!/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' } 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