diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 2f314a0..a185b27 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -26,3 +26,12 @@ S node deployment. The installer must stop on missing prerequisites, invalid public address, occupied ports, failed image verification, failed schema migration, or failed health checks. It must never continue with a partially initialized database. + +PostgreSQL initialization and retries use the fixed application roles +`product_e` and `product_s`. Credentials are generated once and then read back +from `production.env`; a password mismatch is fail-closed. The installer never +logs in as `postgres`, changes a role password, removes volumes by default, or +silently continues after a database error. E's failed volume can only be +removed with `OVERCOOKED_RESET_FAILED_INSTALL=1` plus +`OVERCOOKED_CONFIRM_NO_BUSINESS_DATA=YES`; S database mismatches always require +manual recovery. diff --git a/README.md b/README.md index aefb030..ab39b12 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,16 @@ image. S uses only the schema migration assets published in this repository. If either migration fails, the installer exits non-zero and does not start the corresponding application service. +The fixed PostgreSQL roles are `product_e` and `product_s`; the installer never +uses a `postgres` superuser login. A retry reuses the passwords in the existing +mode-600 `production.env` and keeps named volumes. If a role password cannot be +authenticated, it stops with +`DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY` and does not delete data. +Only a failed E database volume may be reset, and only when both +`OVERCOOKED_RESET_FAILED_INSTALL=1` and +`OVERCOOKED_CONFIRM_NO_BUSINESS_DATA=YES` are explicitly supplied. No +`down -v` or broad volume cleanup is performed. + The installer also exports the approved Media Node image into `/opt/overcooked/node-images/media-node.tar.gz`; S uses that archive when a customer adds a node without a source checkout. diff --git a/SHA256SUMS b/SHA256SUMS index ff8ccab..1bc4868 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,6 +1,10 @@ -3776581f5b8755afe0fbc3ea9121416504f117da83f2baa6214c315d2d473ef8 install.sh +e895daeae728dfc2a54bda1484847ee90465879c981b17b1ab2cfdebcacf2439 install.sh e8cabc5a989d171206361e6ae8b4e0baf9bb9a611ac41106bbe42de0782e8975 compose/s.yml 203b6da918dee21dbfabbcf5466111bd92c7a8c9e754dad61e7e75e20513b880 compose/e.yml c226c5e1feb5cd6948ee9b32897e77240e21793090d8f42234ca999450ec90b0 config/e-server.production.yaml 3e3dc775017577fad3b36b6c21036dfcb6e530793de4c18c825dda888f62e4e3 s-migrate.sh 07f432c281ba90565cd376b42967909b6ff2d19d04454a3ba4e3b184117d45af s-migrations.tar.gz +7845f54d517588953d91f7fbef8be26faccc4dc51b81b6d5fb2d699cd5e86bda test/static-check.sh +7fadbf1f62d2f972a5ae697daf75ff711e6d6bae257f45d0ed9928c9e79e4948 test/database-credentials.sh +e20cc9a6210525b6335b70a1567e35cae01054b1558d17f7593598e672574bda test/fake-docker.sh +306c6ca7407560340797866e077e053627ad409277d1b9da58106fce4cf717cb test/fake-curl.sh diff --git a/install.sh b/install.sh index 7f6ce83..b4dde97 100755 --- a/install.sh +++ b/install.sh @@ -31,6 +31,8 @@ E_PLAYBACK_TICKET_SECRET=not-generated E_PLAYBACK_LEASE_KEY=not-generated E_STORAGE_CREDENTIAL_KEY=not-generated E_PLUGIN_SECRET_KEY=not-generated +RESET_FAILED_INSTALL=${OVERCOOKED_RESET_FAILED_INSTALL:-0} +CONFIRM_NO_BUSINESS_DATA=${OVERCOOKED_CONFIRM_NO_BUSINESS_DATA:-} die() { echo "overcooked-install: $*" >&2; exit 1; } @@ -232,16 +234,46 @@ chown 1000:10001 "$CONTROL_PLANE_DIR/client.key" chmod 755 "$CONTROL_PLANE_DIR" secret() { openssl rand -hex 32; } -S_POSTGRES_PASSWORD=$(secret); S_VALKEY_PASSWORD=$(secret) -S_INTERNAL_SHARED_SECRET=$(secret); S_ACCESS_TOKEN_SECRET=$(secret); S_SETUP_TOKEN=$(secret) -E_POSTGRES_PASSWORD=$(secret); E_PLAYBACK_TICKET_SECRET=$(secret); E_PLAYBACK_LEASE_KEY=$(secret) -E_STORAGE_CREDENTIAL_KEY=$(secret); E_PLUGIN_SECRET_KEY=$(secret) -# Re-runs must reuse credentials that initialized existing database volumes. +read_existing_secret() { + secret_name=$1 + secret_count=$(awk -v key="$secret_name" 'index($0, key "=") == 1 { count++ } END { print count + 0 }' "$ENV_FILE") + [ "$secret_count" = 1 ] || { echo "production.env 中缺少或重复的 $secret_name" >&2; return 1; } + secret_value=$(awk -v key="$secret_name" 'index($0, key "=") == 1 { print substr($0, length(key) + 2); exit }' "$ENV_FILE") + case "$secret_value" in ''|*[!0-9A-Fa-f]*) echo "production.env 中的 $secret_name 不是安全的 hex 值" >&2; return 1 ;; esac + [ "${#secret_value}" = 64 ] || { echo "production.env 中的 $secret_name 长度无效" >&2; return 1; } + printf '%s\n' "$secret_value" +} +read_existing_secret_alias() { + primary_name=$1; legacy_name=$2 + primary_count=$(awk -v key="$primary_name" 'index($0, key "=") == 1 { count++ } END { print count + 0 }' "$ENV_FILE") + legacy_count=$(awk -v key="$legacy_name" 'index($0, key "=") == 1 { count++ } END { print count + 0 }' "$ENV_FILE") + if [ "$primary_count" = 1 ] && [ "$legacy_count" = 0 ]; then + read_existing_secret "$primary_name" + elif [ "$primary_count" = 0 ] && [ "$legacy_count" = 1 ]; then + read_existing_secret "$legacy_name" + else + echo "production.env 中的 $primary_name/$legacy_name 缺少、重复或同时存在" >&2 + return 1 + fi +} if [ -f "$ENV_FILE" ]; then - for name in S_POSTGRES_PASSWORD S_VALKEY_PASSWORD S_INTERNAL_SHARED_SECRET S_ACCESS_TOKEN_SECRET S_SETUP_TOKEN E_POSTGRES_PASSWORD E_PLAYBACK_TICKET_SECRET E_PLAYBACK_LEASE_KEY E_STORAGE_CREDENTIAL_KEY E_PLUGIN_SECRET_KEY; do - value=$(sed -n "s/^${name}=//p" "$ENV_FILE" | head -n 1) - [ -n "$value" ] && eval "$name=\"$value\"" - done + # Re-runs must reuse every generated credential that initialized existing + # volumes. Do not execute arbitrary production.env content. + S_POSTGRES_PASSWORD=$(read_existing_secret S_POSTGRES_PASSWORD) || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法安全读取 S_POSTGRES_PASSWORD" + S_VALKEY_PASSWORD=$(read_existing_secret S_VALKEY_PASSWORD) || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法安全读取 S_VALKEY_PASSWORD" + S_INTERNAL_SHARED_SECRET=$(read_existing_secret S_INTERNAL_SHARED_SECRET) || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法安全读取 S_INTERNAL_SHARED_SECRET" + S_ACCESS_TOKEN_SECRET=$(read_existing_secret S_ACCESS_TOKEN_SECRET) || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法安全读取 S_ACCESS_TOKEN_SECRET" + S_SETUP_TOKEN=$(read_existing_secret S_SETUP_TOKEN) || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法安全读取 S_SETUP_TOKEN" + E_POSTGRES_PASSWORD=$(read_existing_secret_alias PRODUCT_E_POSTGRES_PASSWORD E_POSTGRES_PASSWORD) || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法安全读取 Product E 数据库密码" + E_PLAYBACK_TICKET_SECRET=$(read_existing_secret_alias PRODUCT_E_PLAYBACK_TICKET_SECRET E_PLAYBACK_TICKET_SECRET) || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法安全读取 Product E 播放票据密钥" + E_PLAYBACK_LEASE_KEY=$(read_existing_secret_alias PRODUCT_E_PLAYBACK_LEASE_KEY E_PLAYBACK_LEASE_KEY) || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法安全读取 Product E 租约密钥" + E_STORAGE_CREDENTIAL_KEY=$(read_existing_secret_alias PRODUCT_E_STORAGE_CREDENTIAL_KEY E_STORAGE_CREDENTIAL_KEY) || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法安全读取 Product E 存储凭据密钥" + E_PLUGIN_SECRET_KEY=$(read_existing_secret_alias PRODUCT_E_PLUGIN_SECRET_KEY E_PLUGIN_SECRET_KEY) || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法安全读取 Product E 插件密钥" +else + S_POSTGRES_PASSWORD=$(secret); S_VALKEY_PASSWORD=$(secret) + S_INTERNAL_SHARED_SECRET=$(secret); S_ACCESS_TOKEN_SECRET=$(secret); S_SETUP_TOKEN=$(secret) + E_POSTGRES_PASSWORD=$(secret); E_PLAYBACK_TICKET_SECRET=$(secret); E_PLAYBACK_LEASE_KEY=$(secret) + E_STORAGE_CREDENTIAL_KEY=$(secret); E_PLUGIN_SECRET_KEY=$(secret) fi INSTANCE_SUFFIX=$(hostname 2>/dev/null | tr -cd '[:alnum:]' | cut -c1-24); [ -n "$INSTANCE_SUFFIX" ] || INSTANCE_SUFFIX=customer @@ -363,10 +395,14 @@ chmod 644 "$ROOT/node-images/media-node.tar.gz" wait_for_compose_health() { compose_file=$1; project=$2; service=$3; attempt=1 while [ "$attempt" -le 60 ]; do - container=$(docker compose --env-file "$ENV_FILE" -f "$compose_file" -p "$project" ps -q "$service" 2>/dev/null || true) + if ! container=$(docker compose --env-file "$ENV_FILE" -f "$compose_file" -p "$project" ps -q "$service" 2>/dev/null); then + die "$service 容器状态查询失败" + fi status='' if [ -n "$container" ]; then - status=$(docker inspect -f '{{.State.Health.Status}}' "$container" 2>/dev/null || true) + if ! status=$(docker inspect -f '{{.State.Health.Status}}' "$container" 2>/dev/null); then + die "$service 容器健康状态查询失败" + fi fi [ "$status" = healthy ] && return 0 [ "$status" = unhealthy ] && die "$service 容器健康检查失败" @@ -376,15 +412,66 @@ wait_for_compose_health() { die "$service 容器健康检查超时" } -docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d postgres valkey -# A previous interrupted install may have initialized the volume with a -# different generated role password. Reconcile it through the local Postgres -# socket before running migrations; no data is dropped. -docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" exec -T postgres \ - psql -U postgres -d postgres -v ON_ERROR_STOP=1 \ - -c "ALTER ROLE product_e WITH PASSWORD '$E_POSTGRES_PASSWORD';" >/dev/null +reset_e_postgres_volume() { + compose_file=$1; project=$2 + database_label=E; expected_volume=e-postgres + [ "$RESET_FAILED_INSTALL" = 1 ] && [ "$CONFIRM_NO_BUSINESS_DATA" = YES ] || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: $database_label 数据库密码不匹配;如确认卷中无业务数据,请同时设置 OVERCOOKED_RESET_FAILED_INSTALL=1 OVERCOOKED_CONFIRM_NO_BUSINESS_DATA=YES" + container=$(docker compose --env-file "$ENV_FILE" -f "$compose_file" -p "$project" ps -q postgres 2>/dev/null || true) + [ -n "$container" ] || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 找不到 $database_label postgres 容器" + volume=$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{.Name}}{{end}}{{end}}' "$container" 2>/dev/null || true) + case "$volume" in ''|*[!A-Za-z0-9_.-]*) die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法确认 $database_label 数据卷" ;; esac + volume_project=$(docker volume inspect -f '{{index .Labels "com.docker.compose.project"}}' "$volume" 2>/dev/null || true) + volume_service=$(docker volume inspect -f '{{index .Labels "com.docker.compose.volume"}}' "$volume" 2>/dev/null || true) + [ "$volume_project" = "$project" ] && [ "$volume_service" = "$expected_volume" ] || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: $database_label 数据卷标签不匹配,拒绝删除" + echo "已收到显式无业务数据确认,将删除失败安装的 $database_label PostgreSQL 卷:$volume" >&2 + docker compose --env-file "$ENV_FILE" -f "$compose_file" -p "$project" rm -sf postgres + docker volume rm "$volume" >/dev/null || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法删除 $database_label 数据卷 $volume" +} + +check_e_database_role() { + database_output='' + if database_output=$(printf 'postgres:5432:product_e:product_e:%s\n' "$E_POSTGRES_PASSWORD" | docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" exec -T postgres sh -ceu 'umask 077; cat > /tmp/.overcooked-pgpass; PGPASSFILE=/tmp/.overcooked-pgpass psql -U product_e -d product_e -v ON_ERROR_STOP=1 -Atqc "SELECT current_user"; status=$?; rm -f /tmp/.overcooked-pgpass; exit "$status"' 2>&1); then + [ "$database_output" = product_e ] || { echo "$database_output" | redact_logs >&2; return 1; } + return 0 + fi + echo "$database_output" | redact_logs >&2 + return 1 +} + +check_s_database_role() { + database_output='' + if database_output=$(printf 'postgres:5432:product_s:product_s:%s\n' "$S_POSTGRES_PASSWORD" | docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" exec -T postgres sh -ceu 'umask 077; cat > /tmp/.overcooked-pgpass; PGPASSFILE=/tmp/.overcooked-pgpass psql -U product_s -d product_s -v ON_ERROR_STOP=1 -Atqc "SELECT current_user"; status=$?; rm -f /tmp/.overcooked-pgpass; exit "$status"' 2>&1); then + [ "$database_output" = product_s ] || { echo "$database_output" | redact_logs >&2; return 1; } + return 0 + fi + echo "$database_output" | redact_logs >&2 + return 1 +} + +prepare_e_database() { + docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d postgres valkey + wait_for_compose_health "$E_COMPOSE" "$E_PROJECT" postgres + if check_e_database_role; then return 0; fi + if [ "$RESET_FAILED_INSTALL" = 1 ] && [ "$CONFIRM_NO_BUSINESS_DATA" = YES ]; then + reset_e_postgres_volume "$E_COMPOSE" "$E_PROJECT" + docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d postgres + wait_for_compose_health "$E_COMPOSE" "$E_PROJECT" postgres + check_e_database_role || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 重置后仍无法使用固定 product_e 角色认证" + return 0 + fi + die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: product_e 角色密码不匹配;未删除数据库卷" +} + +prepare_s_database() { + docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d postgres valkey + wait_for_compose_health "$S_COMPOSE" "$S_PROJECT" postgres + if check_s_database_role; then return 0; fi + die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: product_s 角色密码不匹配;未删除数据库卷" +} + +prepare_e_database docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" run --rm --no-deps e-server migrate -config /etc/overcooked-e/config.yaml -docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d postgres valkey +prepare_s_database docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" run --rm --no-deps s-migrate docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d s-control control-proxy webui wait_for_compose_health "$S_COMPOSE" "$S_PROJECT" control-proxy diff --git a/test/database-credentials.sh b/test/database-credentials.sh new file mode 100755 index 0000000..01de3eb --- /dev/null +++ b/test/database-credentials.sh @@ -0,0 +1,100 @@ +#!/bin/sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +fake_bin=$(mktemp -d /tmp/overcooked-installer-fakes.XXXXXX) +test_root=$(mktemp -d /tmp/overcooked-installer-db-test.XXXXXX) +trap 'rm -rf "$fake_bin" "$test_root"' EXIT +cp "$root/test/fake-docker.sh" "$fake_bin/docker" +cp "$root/test/fake-curl.sh" "$fake_bin/curl" +chmod 755 "$fake_bin/docker" "$fake_bin/curl" + +run_install() { + mode=$1 + output=$2 + printf '%s\n' "$mode" > "$test_root/db-state" + : > "$test_root/docker.log" + set +e + PATH="$fake_bin:$PATH" \ + FAKE_DOCKER_LOG="$test_root/docker.log" \ + FAKE_DB_STATE="$test_root/db-state" \ + OVERCOOKED_ROOT="$test_root/root" \ + OVERCOOKED_SERVER_IP=203.0.113.10 \ + OVERCOOKED_API_PORT=29880 \ + OVERCOOKED_WEB_PORT=29883 \ + OVERCOOKED_E_PORT=29896 \ + OVERCOOKED_AUTH_PUBLIC_KEYS='{"auth-1":"PUBLIC_KEY"}' \ + OVERCOOKED_ACTIVATION_CODE=TEST-CODE \ + "$root/install.sh" > "$output" 2>&1 + status=$? + set -e + if [ "$status" -ne 0 ]; then + cat "$output" >&2 + fi + return "$status" +} + +first_output=$test_root/first.out +run_install ok "$first_output" +first_e_password=$(sed -n 's/^PRODUCT_E_POSTGRES_PASSWORD=//p' "$test_root/root/production.env") +first_s_password=$(sed -n 's/^S_POSTGRES_PASSWORD=//p' "$test_root/root/production.env") +[ "${#first_e_password}" = 64 ] +[ "${#first_s_password}" = 64 ] +[ "$(stat -c '%a' "$test_root/root/production.env")" = 600 ] +! rg -n 'volume rm|down .*-[^ ]*v' "$test_root/docker.log" + +second_output=$test_root/second.out +run_install ok "$second_output" +second_e_password=$(sed -n 's/^PRODUCT_E_POSTGRES_PASSWORD=//p' "$test_root/root/production.env") +second_s_password=$(sed -n 's/^S_POSTGRES_PASSWORD=//p' "$test_root/root/production.env") +[ "$first_e_password" = "$second_e_password" ] +[ "$first_s_password" = "$second_s_password" ] +! rg -n 'volume rm|down .*-[^ ]*v' "$test_root/docker.log" +echo "MALICIOUS=\$(touch $test_root/marker)" >> "$test_root/root/production.env" +third_output=$test_root/third.out +run_install ok "$third_output" +[ ! -e "$test_root/marker" ] + +mismatch_output=$test_root/mismatch.out +if run_install mismatch "$mismatch_output"; then + echo 'password mismatch unexpectedly succeeded' >&2 + exit 1 +fi +rg -q 'DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY' "$mismatch_output" +! rg -n '^volume rm|down .*-[^ ]*v' "$test_root/docker.log" +[ "$first_e_password" = "$(sed -n 's/^PRODUCT_E_POSTGRES_PASSWORD=//p' "$test_root/root/production.env")" ] + +wrong_role_output=$test_root/wrong-role.out +if run_install wrong-role "$wrong_role_output"; then + echo 'wrong database role unexpectedly succeeded' >&2 + exit 1 +fi +rg -q 'DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY' "$wrong_role_output" +! rg -n '^volume rm|down .*-[^ ]*v' "$test_root/docker.log" + +s_mismatch_output=$test_root/s-mismatch.out +if run_install s-mismatch "$s_mismatch_output"; then + echo 'S password mismatch unexpectedly succeeded' >&2 + exit 1 +fi +rg -q 'DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY' "$s_mismatch_output" +! rg -n '^volume rm|down .*-[^ ]*v' "$test_root/docker.log" + +reset_output=$test_root/reset.out +printf 'reset-once\n' > "$test_root/db-state" +PATH="$fake_bin:$PATH" \ + FAKE_DOCKER_LOG="$test_root/docker.log" \ + FAKE_DB_STATE="$test_root/db-state" \ + OVERCOOKED_ROOT="$test_root/root" \ + OVERCOOKED_SERVER_IP=203.0.113.10 \ + OVERCOOKED_API_PORT=29880 \ + OVERCOOKED_WEB_PORT=29883 \ + OVERCOOKED_E_PORT=29896 \ + OVERCOOKED_AUTH_PUBLIC_KEYS='{"auth-1":"PUBLIC_KEY"}' \ + OVERCOOKED_ACTIVATION_CODE=TEST-CODE \ + OVERCOOKED_RESET_FAILED_INSTALL=1 \ + OVERCOOKED_CONFIRM_NO_BUSINESS_DATA=YES \ + "$root/install.sh" > "$reset_output" 2>&1 +rg -q '已收到显式无业务数据确认' "$reset_output" +rg -q '^volume rm fake-e-postgres$' "$test_root/docker.log" +echo 'database credential retry/reset tests passed' diff --git a/test/fake-curl.sh b/test/fake-curl.sh new file mode 100755 index 0000000..039e4d0 --- /dev/null +++ b/test/fake-curl.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 diff --git a/test/fake-docker.sh b/test/fake-docker.sh new file mode 100755 index 0000000..521252b --- /dev/null +++ b/test/fake-docker.sh @@ -0,0 +1,81 @@ +#!/bin/sh +set -eu + +: "${FAKE_DOCKER_LOG:?}" +: "${FAKE_DB_STATE:?}" +printf '%s\n' "$*" >> "$FAKE_DOCKER_LOG" + +if [ "${1:-}" = compose ]; then + shift + args=$* + case "$args" in + *' version'*) exit 0 ;; + *' ps -q postgres'*) printf 'fake-postgres\n'; exit 0 ;; + *' ps -q control-proxy'*) printf 'fake-control-proxy\n'; exit 0 ;; + *' exec '*product_e*) + if [ "$(cat "$FAKE_DB_STATE")" = mismatch ]; then + printf 'FATAL: password authentication failed for user product_e\n' >&2 + exit 2 + fi + if [ "$(cat "$FAKE_DB_STATE")" = reset-once ]; then + printf 'FATAL: password authentication failed for user product_e\n' >&2 + exit 2 + fi + if [ "$(cat "$FAKE_DB_STATE")" = wrong-role ]; then + printf 'product_s\n' + exit 0 + fi + printf 'product_e\n'; exit 0 ;; + *' exec '*product_s*) + if [ "$(cat "$FAKE_DB_STATE")" = wrong-role ]; then + printf 'product_e\n' + exit 0 + fi + if [ "$(cat "$FAKE_DB_STATE")" = s-mismatch ]; then + printf 'FATAL: password authentication failed for user product_s\n' >&2 + exit 2 + fi + printf 'product_s\n'; exit 0 ;; + *) exit 0 ;; + esac +fi + +case "${1:-}" in + network|pull|image) exit 0 ;; + save) + output='' + previous='' + for argument do + if [ "$previous" = -o ]; then output=$argument; fi + previous=$argument + done + [ -n "$output" ] && : > "$output" + exit 0 + ;; + inspect) + format=${3:-} + case "$format" in + *State.Health.Status*) printf 'healthy\n' ;; + *var/lib/postgresql/data*) printf 'fake-e-postgres\n' ;; + *) printf '\n' ;; + esac + exit 0 + ;; + volume) + case "${2:-}" in + inspect) + case "${4:-}" in + *com.docker.compose.project*) printf 'overcooked-e\n' ;; + *com.docker.compose.volume*) printf 'e-postgres\n' ;; + esac + exit 0 + ;; + rm) + printf 'ok\n' > "$FAKE_DB_STATE" + exit 0 + ;; + esac + ;; +esac + +exit 0 diff --git a/test/static-check.sh b/test/static-check.sh index 56c4ad7..3294ca6 100755 --- a/test/static-check.sh +++ b/test/static-check.sh @@ -16,11 +16,15 @@ test -f compose/s.yml test -f compose/e.yml test -f config/e-server.production.yaml test -f s-migrations.tar.gz -test "$(find s-migrations -type f -name '*.up.sql' | wc -l)" -gt 0 +test "$(tar -tzf s-migrations.tar.gz | awk '/\.up\.sql$/ { count++ } END { print count + 0 }')" -gt 0 +test -x test/database-credentials.sh sha256sum -c SHA256SUMS >/dev/null ! rg -n '^([[:space:]]*)build:' compose ! rg -n 'overcooked-auth|overcooked-auth-webui' compose install.sh ! rg -n 'S_TURNSTILE_(SITE|SECRET)_KEY: .*\\?required' compose ! rg -n 'S_INTERNAL_TLS_(ADDR|CERT_FILE|KEY_FILE|CLIENT_CA_FILE)' compose install.sh +! rg -n 'psql[[:space:]]+-U[[:space:]]+postgres|(^|[^[:alnum:]_])eval([[:space:]]|$)' install.sh +rg -n 'psql[[:space:]]+-U[[:space:]]+product_e|psql[[:space:]]+-U[[:space:]]+product_s' install.sh >/dev/null rg -n 'control-proxy|PRODUCT_E_CONTROL_PLANE_TLS_' compose install.sh >/dev/null +./test/database-credentials.sh echo 'static customer installer checks passed'