diff --git a/Dockerfile b/Dockerfile index 6eea372..45b8a8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,10 @@ FROM node:22-bookworm-slim WORKDIR /app +RUN apt-get update -y \ + && apt-get install -y --no-install-recommends curl jq ca-certificates \ + && rm -rf /var/lib/apt/lists/* + COPY package*.json ./ COPY prisma ./prisma COPY prisma.config.ts ./ diff --git a/scripts/load-vault-env.sh b/scripts/load-vault-env.sh index c64d1fd..fb3e25e 100755 --- a/scripts/load-vault-env.sh +++ b/scripts/load-vault-env.sh @@ -1,34 +1,170 @@ #!/bin/sh set -eu -if [ "${VAULT_ENABLED:-auto}" = "false" ] || [ "${VAULT_ENABLED:-auto}" = "0" ]; then +log() { + printf '%s\n' "$*" >&2 +} + +is_enabled() { + case "${1:-}" in + 1|true|TRUE|yes|YES|on|ON) return 0 ;; + *) return 1 ;; + esac +} + +wait_for_http() { + name="$1" + url="$2" + timeout="$3" + interval="$4" + mode="${5:-strict}" + start_ts="$(date +%s)" + + while :; do + if [ "$mode" = "connect" ]; then + if curl -sS --max-time 3 "$url" >/dev/null 2>&1; then + log "$name is reachable at $url." + return 0 + fi + else + if curl -fsS --max-time 3 "$url" >/dev/null 2>&1; then + log "$name is reachable at $url." + return 0 + fi + fi + + now_ts="$(date +%s)" + if [ $((now_ts - start_ts)) -ge "$timeout" ]; then + log "Timeout waiting for $name at $url after ${timeout}s." + return 1 + fi + + sleep "$interval" + done +} + +wait_for_tcp() { + name="$1" + host="$2" + port="$3" + timeout="$4" + interval="$5" + start_ts="$(date +%s)" + + while :; do + if node -e "const net=require('net');const host=process.argv[1];const port=Number(process.argv[2]);const socket=net.connect({host,port});const fail=()=>{socket.destroy();process.exit(1)};socket.setTimeout(2000,fail);socket.on('connect',()=>{socket.end();process.exit(0)});socket.on('error',fail);" "$host" "$port" >/dev/null 2>&1; then + log "$name is reachable at ${host}:${port}." + return 0 + fi + + now_ts="$(date +%s)" + if [ $((now_ts - start_ts)) -ge "$timeout" ]; then + log "Timeout waiting for $name at ${host}:${port} after ${timeout}s." + return 1 + fi + + sleep "$interval" + done +} + +VAULT_ENABLED="${VAULT_ENABLED:-auto}" +if [ "$VAULT_ENABLED" = "false" ] || [ "$VAULT_ENABLED" = "0" ]; then return 0 2>/dev/null || exit 0 fi if [ -z "${VAULT_ADDR:-}" ] || [ -z "${VAULT_TOKEN:-}" ]; then - if [ "${VAULT_ENABLED:-auto}" = "true" ] || [ "${VAULT_ENABLED:-auto}" = "1" ]; then - echo "VAULT_ENABLED=true but VAULT_ADDR/VAULT_TOKEN are not set" >&2 + if [ "$VAULT_ENABLED" = "true" ] || [ "$VAULT_ENABLED" = "1" ]; then + log "Vault bootstrap is required but VAULT_ADDR or VAULT_TOKEN is missing." return 1 2>/dev/null || exit 1 fi return 0 2>/dev/null || exit 0 fi if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then - echo "Vault bootstrap requires curl and jq." >&2 + log "Vault bootstrap requires curl and jq." return 1 2>/dev/null || exit 1 fi -kv_mount="${VAULT_KV_MOUNT:-secret}" +VAULT_KV_MOUNT="${VAULT_KV_MOUNT:-secret}" +VAULT_WAIT_TIMEOUT_SECONDS="${VAULT_WAIT_TIMEOUT_SECONDS:-90}" +VAULT_WAIT_INTERVAL_SECONDS="${VAULT_WAIT_INTERVAL_SECONDS:-2}" -load_path() { +wait_for_http \ + "Vault" \ + "${VAULT_ADDR%/}/v1/sys/health?standbyok=true&perfstandbyok=true" \ + "$VAULT_WAIT_TIMEOUT_SECONDS" \ + "$VAULT_WAIT_INTERVAL_SECONDS" \ + "strict" + +load_secret_path() { path="$1" - [ -z "$path" ] && return 0 + source_name="$2" + if [ -z "$path" ]; then + return 0 + fi - payload="$(curl -fsS -H "X-Vault-Token: ${VAULT_TOKEN}" "${VAULT_ADDR%/}/v1/${kv_mount}/data/${path}")" - echo "$payload" | jq -r '.data.data // {} | to_entries[] | "\(.key)=\(.value|tostring)"' | while IFS='=' read -r k v; do - export "$k=$v" + url="${VAULT_ADDR%/}/v1/${VAULT_KV_MOUNT}/data/${path}" + response="$(curl -fsS -H "X-Vault-Token: $VAULT_TOKEN" "$url")" || { + log "Failed to load Vault path ${VAULT_KV_MOUNT}/${path}." + return 1 + } + + encoded_items="$(printf '%s' "$response" | jq -r '.data.data // {} | to_entries[]? | @base64')" + if [ -z "$encoded_items" ]; then + return 0 + fi + + old_ifs="${IFS}" + IFS=' +' + for encoded_item in $encoded_items; do + key="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.key')" + value="$(printf '%s' "$encoded_item" | base64 -d | jq -r '.value | tostring')" + export "$key=$value" done + IFS="${old_ifs}" + + log "Loaded Vault ${source_name} secrets from ${VAULT_KV_MOUNT}/${path}." } -load_path "${VAULT_SHARED_PATH:-}" -load_path "${VAULT_PROJECT_PATH:-}" +load_secret_path "${VAULT_SHARED_PATH:-}" "shared" +load_secret_path "${VAULT_PROJECT_PATH:-}" "project" + +if is_enabled "${WAIT_FOR_HATCHET_ENGINE:-0}"; then + HATCHET_WAIT_TIMEOUT_SECONDS="${HATCHET_WAIT_TIMEOUT_SECONDS:-120}" + HATCHET_WAIT_INTERVAL_SECONDS="${HATCHET_WAIT_INTERVAL_SECONDS:-2}" + hatchet_wait_host_port="${HATCHET_WAIT_HOST_PORT:-${HATCHET_CLIENT_HOST_PORT:-}}" + hatchet_wait_url="${HATCHET_WAIT_URL:-${HATCHET_CLIENT_API_URL:-}}" + + if [ -n "$hatchet_wait_host_port" ]; then + case "$hatchet_wait_host_port" in + *:*) + hatchet_wait_host="${hatchet_wait_host_port%:*}" + hatchet_wait_port="${hatchet_wait_host_port##*:}" + ;; + *) + log "Invalid HATCHET wait host:port value: ${hatchet_wait_host_port}" + return 1 2>/dev/null || exit 1 + ;; + esac + + wait_for_tcp \ + "Hatchet engine" \ + "$hatchet_wait_host" \ + "$hatchet_wait_port" \ + "$HATCHET_WAIT_TIMEOUT_SECONDS" \ + "$HATCHET_WAIT_INTERVAL_SECONDS" + else + if [ -z "$hatchet_wait_url" ]; then + log "WAIT_FOR_HATCHET_ENGINE is enabled but no HATCHET wait target is configured." + return 1 2>/dev/null || exit 1 + fi + + wait_for_http \ + "Hatchet engine" \ + "${hatchet_wait_url%/}/" \ + "$HATCHET_WAIT_TIMEOUT_SECONDS" \ + "$HATCHET_WAIT_INTERVAL_SECONDS" \ + "connect" + fi +fi