Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
330 lines
9.7 KiB
Bash
Executable File
330 lines
9.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -Eeuo pipefail
|
|
|
|
DOWNLOAD_BASE="${RAKAZO_DOWNLOAD_BASE:-https://raw.githubusercontent.com/elie222/rakazo/main/infra/compose}"
|
|
while [[ "$DOWNLOAD_BASE" == */ ]]; do
|
|
DOWNLOAD_BASE="${DOWNLOAD_BASE%/}"
|
|
done
|
|
case "$DOWNLOAD_BASE" in
|
|
https://*) ;;
|
|
*)
|
|
echo "Rakazo setup failed: RAKAZO_DOWNLOAD_BASE must use https." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
readonly DOWNLOAD_BASE
|
|
|
|
readonly COMPOSE_FILE="docker-compose.images.yml"
|
|
readonly ENV_EXAMPLE=".env.images.example"
|
|
readonly ENV_FILE=".env"
|
|
|
|
prepare_only=false
|
|
skip_existing=false
|
|
pull_never=false
|
|
if [[ "${RAKAZO_DOWNLOAD_SKIP_EXISTING:-}" == "1" ]]; then
|
|
skip_existing=true
|
|
fi
|
|
if [[ "${RAKAZO_PULL_NEVER:-}" == "1" ]]; then
|
|
pull_never=true
|
|
fi
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--prepare-only)
|
|
prepare_only=true
|
|
;;
|
|
--local)
|
|
skip_existing=true
|
|
;;
|
|
--pull-never)
|
|
pull_never=true
|
|
;;
|
|
--offline)
|
|
# Air-gap bootstrap: keep local Compose/env files and do not pull images.
|
|
skip_existing=true
|
|
pull_never=true
|
|
;;
|
|
*)
|
|
echo "Usage: bash install-images.sh [--prepare-only] [--local] [--pull-never] [--offline]" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
temporary_file=""
|
|
cleanup() {
|
|
if [[ -n "$temporary_file" ]]; then
|
|
rm -f -- "$temporary_file"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
fail() {
|
|
echo "Rakazo setup failed: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
for command_name in curl docker openssl; do
|
|
command -v "$command_name" >/dev/null 2>&1 || fail "'$command_name' is required."
|
|
done
|
|
|
|
docker compose version >/dev/null 2>&1 || fail "the Docker Compose plugin is required."
|
|
|
|
# Optional proxy knobs from an existing .env (operators often set them there for
|
|
# containers). Do not override values already present in the shell. Treat each
|
|
# HTTP/HTTPS/NO_PROXY pair as one family so either case in the shell wins.
|
|
# Within .env, later assignments for a family win (shell presence is snapshotted
|
|
# before the file is read). Comment stripping is quote-aware so `#` inside
|
|
# matching quotes is kept; this is not a full dotenv parser.
|
|
load_proxy_vars_from_env_file() {
|
|
local file="$1"
|
|
local line key value
|
|
local i c quote out
|
|
local shell_http=0 shell_https=0 shell_no=0
|
|
[[ -f "$file" ]] || return 0
|
|
[[ -n "${HTTP_PROXY+x}" || -n "${http_proxy+x}" ]] && shell_http=1
|
|
[[ -n "${HTTPS_PROXY+x}" || -n "${https_proxy+x}" ]] && shell_https=1
|
|
[[ -n "${NO_PROXY+x}" || -n "${no_proxy+x}" ]] && shell_no=1
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
# Windows/.editorconfig CRLF: drop trailing CR so quoted values still match.
|
|
line="${line%$'\r'}"
|
|
out=""
|
|
quote=""
|
|
for ((i = 0; i < ${#line}; i++)); do
|
|
c="${line:i:1}"
|
|
if [[ -n "$quote" ]]; then
|
|
# Inside double quotes, treat \" and \\ as escaped so \# stays in-value.
|
|
if [[ "$quote" == '"' && "$c" == '\' ]] && ((i + 1 < ${#line})); then
|
|
out+="$c"
|
|
i=$((i + 1))
|
|
out+="${line:i:1}"
|
|
continue
|
|
fi
|
|
out+="$c"
|
|
[[ "$c" == "$quote" ]] && quote=""
|
|
elif [[ "$c" == "'" || "$c" == '"' ]]; then
|
|
quote="$c"
|
|
out+="$c"
|
|
elif [[ "$c" == "#" ]]; then
|
|
break
|
|
else
|
|
out+="$c"
|
|
fi
|
|
done
|
|
line="$out"
|
|
[[ "$line" =~ ^[[:space:]]*(HTTP_PROXY|HTTPS_PROXY|NO_PROXY|http_proxy|https_proxy|no_proxy)=(.*)$ ]] || continue
|
|
key="${BASH_REMATCH[1]}"
|
|
value="${BASH_REMATCH[2]}"
|
|
value="${value#"${value%%[![:space:]]*}"}"
|
|
value="${value%"${value##*[![:space:]]}"}"
|
|
if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then
|
|
value="${value:1:${#value}-2}"
|
|
value="${value//\\\"/\"}"
|
|
value="${value//\\\\/\\}"
|
|
elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then
|
|
value="${value:1:${#value}-2}"
|
|
fi
|
|
case "$key" in
|
|
HTTP_PROXY|http_proxy)
|
|
if ((shell_http)); then
|
|
continue
|
|
fi
|
|
unset -v HTTP_PROXY http_proxy
|
|
;;
|
|
HTTPS_PROXY|https_proxy)
|
|
if ((shell_https)); then
|
|
continue
|
|
fi
|
|
unset -v HTTPS_PROXY https_proxy
|
|
;;
|
|
NO_PROXY|no_proxy)
|
|
if ((shell_no)); then
|
|
continue
|
|
fi
|
|
unset -v NO_PROXY no_proxy
|
|
;;
|
|
esac
|
|
export "${key}=${value}"
|
|
done <"$file"
|
|
}
|
|
|
|
# curl uses lowercase http_proxy for http:// URLs; many Mainland hosts only export HTTP_PROXY.
|
|
sync_curl_proxy_env() {
|
|
if [[ -n "${HTTP_PROXY+x}" && -z "${http_proxy+x}" ]]; then
|
|
export http_proxy="$HTTP_PROXY"
|
|
fi
|
|
if [[ -n "${HTTPS_PROXY+x}" && -z "${https_proxy+x}" ]]; then
|
|
export https_proxy="$HTTPS_PROXY"
|
|
fi
|
|
if [[ -n "${NO_PROXY+x}" && -z "${no_proxy+x}" ]]; then
|
|
export no_proxy="$NO_PROXY"
|
|
fi
|
|
}
|
|
|
|
prepare_proxy_env() {
|
|
load_proxy_vars_from_env_file "$ENV_FILE"
|
|
sync_curl_proxy_env
|
|
}
|
|
prepare_proxy_env
|
|
|
|
curl_download() {
|
|
local url="$1"
|
|
local out="$2"
|
|
local attempt
|
|
local max_attempts=3
|
|
|
|
if curl --help all 2>/dev/null | grep -q -- '--retry-all-errors'; then
|
|
curl -fsSL --proto-redir =https --retry 3 --retry-delay 2 --retry-all-errors "$url" -o "$out"
|
|
return $?
|
|
fi
|
|
|
|
attempt=1
|
|
while [[ "$attempt" -le "$max_attempts" ]]; do
|
|
if curl -fsSL --proto-redir =https "$url" -o "$out"; then
|
|
return 0
|
|
fi
|
|
if [[ "$attempt" -eq "$max_attempts" ]]; then
|
|
return 1
|
|
fi
|
|
sleep 2
|
|
attempt=$((attempt + 1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
download() {
|
|
local filename="$1"
|
|
local url="${DOWNLOAD_BASE}/${filename}"
|
|
|
|
if [[ "$skip_existing" == true && -e "$filename" ]]; then
|
|
echo "Using local ${filename}"
|
|
return 0
|
|
fi
|
|
|
|
temporary_file=$(mktemp "./${filename}.tmp.XXXXXX")
|
|
if ! curl_download "$url" "$temporary_file"; then
|
|
fail "could not download ${filename} from ${url}. Set HTTP_PROXY/HTTPS_PROXY in the shell or .env (NO_PROXY for localhost), or pre-place the file and use --local / --offline."
|
|
fi
|
|
mv -- "$temporary_file" "$filename"
|
|
temporary_file=""
|
|
}
|
|
|
|
create_env() {
|
|
umask 077
|
|
temporary_file=$(mktemp "./${ENV_FILE}.tmp.XXXXXX")
|
|
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
case "$line" in
|
|
"POSTGRES_PASSWORD=")
|
|
printf 'POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 16)"
|
|
;;
|
|
"BETTER_AUTH_SECRET=")
|
|
printf 'BETTER_AUTH_SECRET=%s\n' "$(openssl rand -hex 32)"
|
|
;;
|
|
"ENCRYPTION_KEY=")
|
|
printf 'ENCRYPTION_KEY=%s\n' "$(openssl rand -hex 32)"
|
|
;;
|
|
"SCREEN_PROXY_SECRET=")
|
|
printf 'SCREEN_PROXY_SECRET=%s\n' "$(openssl rand -hex 32)"
|
|
;;
|
|
"SANDBOX_SUPERVISOR_TOKEN=")
|
|
printf 'SANDBOX_SUPERVISOR_TOKEN=%s\n' "$(openssl rand -hex 32)"
|
|
;;
|
|
*)
|
|
printf '%s\n' "$line"
|
|
;;
|
|
esac
|
|
done < "$ENV_EXAMPLE" > "$temporary_file"
|
|
|
|
chmod 600 "$temporary_file"
|
|
mv -- "$temporary_file" "$ENV_FILE"
|
|
temporary_file=""
|
|
echo "Created .env with random secrets."
|
|
}
|
|
|
|
validate_required_secrets() {
|
|
if ! docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" -f - config --environment <<'YAML' | awk '
|
|
BEGIN {
|
|
required["POSTGRES_PASSWORD"] = 1
|
|
required["BETTER_AUTH_SECRET"] = 1
|
|
required["ENCRYPTION_KEY"] = 1
|
|
required["SCREEN_PROXY_SECRET"] = 1
|
|
required["SANDBOX_SUPERVISOR_TOKEN"] = 1
|
|
}
|
|
{
|
|
name = $0
|
|
sub(/=.*/, "", name)
|
|
if (!(name in required)) next
|
|
seen[name]++
|
|
|
|
value = $0
|
|
sub(/^[^=]*=/, "", value)
|
|
gsub(/[[:space:]]/, "", value)
|
|
if (value != "") nonempty[name]++
|
|
}
|
|
END {
|
|
for (name in required) {
|
|
if (seen[name] != 1 || nonempty[name] != 1) exit 1
|
|
}
|
|
}
|
|
'
|
|
services:
|
|
api:
|
|
environment:
|
|
_RAKAZO_VALIDATE_POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
|
|
_RAKAZO_VALIDATE_BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?Set BETTER_AUTH_SECRET in .env}
|
|
_RAKAZO_VALIDATE_ENCRYPTION_KEY: ${ENCRYPTION_KEY:?Set ENCRYPTION_KEY in .env}
|
|
_RAKAZO_VALIDATE_SCREEN_PROXY_SECRET: ${SCREEN_PROXY_SECRET:?Set SCREEN_PROXY_SECRET in .env}
|
|
_RAKAZO_VALIDATE_SANDBOX_SUPERVISOR_TOKEN: ${SANDBOX_SUPERVISOR_TOKEN:?Set SANDBOX_SUPERVISOR_TOKEN in .env}
|
|
YAML
|
|
then
|
|
fail "set every required secret in .env to a non-empty value."
|
|
fi
|
|
}
|
|
|
|
download "$COMPOSE_FILE"
|
|
download "$ENV_EXAMPLE"
|
|
|
|
if [[ -e "$ENV_FILE" ]]; then
|
|
echo "Keeping existing .env."
|
|
else
|
|
create_env
|
|
fi
|
|
|
|
validate_required_secrets
|
|
|
|
if [[ "$prepare_only" == true ]]; then
|
|
echo "Rakazo files are ready. Edit .env, then run: bash install-images.sh"
|
|
exit 0
|
|
fi
|
|
|
|
prepare_proxy_env
|
|
if [[ "$pull_never" == true ]]; then
|
|
echo "Skipping image pull (--pull-never / --offline); images must already be on this Docker host."
|
|
else
|
|
if ! docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" pull; then
|
|
fail "could not pull images. Shell HTTP_PROXY often does not reach the Docker daemon. Configure daemon proxy/registry-mirrors, set image env vars to a reachable registry, or preload images then compose up with --pull never."
|
|
fi
|
|
fi
|
|
# `--wait` without `--wait-timeout` can hang on one-shot services (Compose < 2.7)
|
|
# or never return if a healthcheck stays red (Compose < 2.17). Prefer both flags.
|
|
compose_up_help=$(docker compose up --help 2>/dev/null || true)
|
|
up_pull_args=()
|
|
if [[ "$pull_never" == true ]]; then
|
|
if grep -q -- '--pull' <<<"$compose_up_help"; then
|
|
up_pull_args=(--pull never)
|
|
else
|
|
echo "cannot enforce pull-never on this Compose version; startup fails if an image is missing locally" >&2
|
|
fi
|
|
fi
|
|
# bash 3.2 + set -u: "${arr[@]}" aborts when arr is empty.
|
|
if grep -q -- '--wait-timeout' <<<"$compose_up_help"; then
|
|
echo "Waiting for healthy services."
|
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d ${up_pull_args[@]+"${up_pull_args[@]}"} --wait --wait-timeout 300
|
|
else
|
|
docker compose --env-file "$ENV_FILE" -f "$COMPOSE_FILE" up -d ${up_pull_args[@]+"${up_pull_args[@]}"}
|
|
fi
|
|
|
|
echo "Rakazo is starting at http://127.0.0.1:5173"
|