fix(deploy): fail closed on postgres password mismatch
This commit is contained in:
1 parent
d140a47f23
commit
aea8cd7d60
8 files changed
+318
-21
No files matched your search
@@ -26,3 +26,12 @@ S node deployment.
|
|||||||
The installer must stop on missing prerequisites, invalid public address,
|
The installer must stop on missing prerequisites, invalid public address,
|
||||||
occupied ports, failed image verification, failed schema migration, or failed
|
occupied ports, failed image verification, failed schema migration, or failed
|
||||||
health checks. It must never continue with a partially initialized database.
|
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.
|
||||||
@@ -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
|
If either migration fails, the installer exits non-zero and does not start the
|
||||||
corresponding application service.
|
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
|
The installer also exports the approved Media Node image into
|
||||||
`/opt/overcooked/node-images/media-node.tar.gz`; S uses that archive when a
|
`/opt/overcooked/node-images/media-node.tar.gz`; S uses that archive when a
|
||||||
customer adds a node without a source checkout.
|
customer adds a node without a source checkout.
|
||||||
|
|||||||
+5
-1
@@ -1,6 +1,10 @@
|
|||||||
3776581f5b8755afe0fbc3ea9121416504f117da83f2baa6214c315d2d473ef8 install.sh
|
e895daeae728dfc2a54bda1484847ee90465879c981b17b1ab2cfdebcacf2439 install.sh
|
||||||
e8cabc5a989d171206361e6ae8b4e0baf9bb9a611ac41106bbe42de0782e8975 compose/s.yml
|
e8cabc5a989d171206361e6ae8b4e0baf9bb9a611ac41106bbe42de0782e8975 compose/s.yml
|
||||||
203b6da918dee21dbfabbcf5466111bd92c7a8c9e754dad61e7e75e20513b880 compose/e.yml
|
203b6da918dee21dbfabbcf5466111bd92c7a8c9e754dad61e7e75e20513b880 compose/e.yml
|
||||||
c226c5e1feb5cd6948ee9b32897e77240e21793090d8f42234ca999450ec90b0 config/e-server.production.yaml
|
c226c5e1feb5cd6948ee9b32897e77240e21793090d8f42234ca999450ec90b0 config/e-server.production.yaml
|
||||||
3e3dc775017577fad3b36b6c21036dfcb6e530793de4c18c825dda888f62e4e3 s-migrate.sh
|
3e3dc775017577fad3b36b6c21036dfcb6e530793de4c18c825dda888f62e4e3 s-migrate.sh
|
||||||
07f432c281ba90565cd376b42967909b6ff2d19d04454a3ba4e3b184117d45af s-migrations.tar.gz
|
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
|
||||||
+106
-19
@@ -31,6 +31,8 @@ E_PLAYBACK_TICKET_SECRET=not-generated
|
|||||||
E_PLAYBACK_LEASE_KEY=not-generated
|
E_PLAYBACK_LEASE_KEY=not-generated
|
||||||
E_STORAGE_CREDENTIAL_KEY=not-generated
|
E_STORAGE_CREDENTIAL_KEY=not-generated
|
||||||
E_PLUGIN_SECRET_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; }
|
die() { echo "overcooked-install: $*" >&2; exit 1; }
|
||||||
|
|
||||||
@@ -232,16 +234,46 @@ chown 1000:10001 "$CONTROL_PLANE_DIR/client.key"
|
|||||||
chmod 755 "$CONTROL_PLANE_DIR"
|
chmod 755 "$CONTROL_PLANE_DIR"
|
||||||
|
|
||||||
secret() { openssl rand -hex 32; }
|
secret() { openssl rand -hex 32; }
|
||||||
S_POSTGRES_PASSWORD=$(secret); S_VALKEY_PASSWORD=$(secret)
|
read_existing_secret() {
|
||||||
S_INTERNAL_SHARED_SECRET=$(secret); S_ACCESS_TOKEN_SECRET=$(secret); S_SETUP_TOKEN=$(secret)
|
secret_name=$1
|
||||||
E_POSTGRES_PASSWORD=$(secret); E_PLAYBACK_TICKET_SECRET=$(secret); E_PLAYBACK_LEASE_KEY=$(secret)
|
secret_count=$(awk -v key="$secret_name" 'index($0, key "=") == 1 { count++ } END { print count + 0 }' "$ENV_FILE")
|
||||||
E_STORAGE_CREDENTIAL_KEY=$(secret); E_PLUGIN_SECRET_KEY=$(secret)
|
[ "$secret_count" = 1 ] || { echo "production.env 中缺少或重复的 $secret_name" >&2; return 1; }
|
||||||
# Re-runs must reuse credentials that initialized existing database volumes.
|
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
|
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
|
# Re-runs must reuse every generated credential that initialized existing
|
||||||
value=$(sed -n "s/^${name}=//p" "$ENV_FILE" | head -n 1)
|
# volumes. Do not execute arbitrary production.env content.
|
||||||
[ -n "$value" ] && eval "$name=\"$value\""
|
S_POSTGRES_PASSWORD=$(read_existing_secret S_POSTGRES_PASSWORD) || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 无法安全读取 S_POSTGRES_PASSWORD"
|
||||||
done
|
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
|
fi
|
||||||
INSTANCE_SUFFIX=$(hostname 2>/dev/null | tr -cd '[:alnum:]' | cut -c1-24); [ -n "$INSTANCE_SUFFIX" ] || INSTANCE_SUFFIX=customer
|
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() {
|
wait_for_compose_health() {
|
||||||
compose_file=$1; project=$2; service=$3; attempt=1
|
compose_file=$1; project=$2; service=$3; attempt=1
|
||||||
while [ "$attempt" -le 60 ]; do
|
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=''
|
status=''
|
||||||
if [ -n "$container" ]; then
|
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
|
fi
|
||||||
[ "$status" = healthy ] && return 0
|
[ "$status" = healthy ] && return 0
|
||||||
[ "$status" = unhealthy ] && die "$service 容器健康检查失败"
|
[ "$status" = unhealthy ] && die "$service 容器健康检查失败"
|
||||||
@@ -376,15 +412,66 @@ wait_for_compose_health() {
|
|||||||
die "$service 容器健康检查超时"
|
die "$service 容器健康检查超时"
|
||||||
}
|
}
|
||||||
|
|
||||||
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d postgres valkey
|
reset_e_postgres_volume() {
|
||||||
# A previous interrupted install may have initialized the volume with a
|
compose_file=$1; project=$2
|
||||||
# different generated role password. Reconcile it through the local Postgres
|
database_label=E; expected_volume=e-postgres
|
||||||
# socket before running migrations; no data is dropped.
|
[ "$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"
|
||||||
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" exec -T postgres \
|
container=$(docker compose --env-file "$ENV_FILE" -f "$compose_file" -p "$project" ps -q postgres 2>/dev/null || true)
|
||||||
psql -U postgres -d postgres -v ON_ERROR_STOP=1 \
|
[ -n "$container" ] || die "DATABASE_PASSWORD_MISMATCH_NEEDS_MANUAL_RECOVERY: 找不到 $database_label postgres 容器"
|
||||||
-c "ALTER ROLE product_e WITH PASSWORD '$E_POSTGRES_PASSWORD';" >/dev/null
|
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 "$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" 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
|
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
|
wait_for_compose_health "$S_COMPOSE" "$S_PROJECT" control-proxy
|
||||||
|
|||||||
Executable
+100
@@ -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'
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
exit 0
|
||||||
Executable
+81
@@ -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
|
||||||
@@ -16,11 +16,15 @@ test -f compose/s.yml
|
|||||||
test -f compose/e.yml
|
test -f compose/e.yml
|
||||||
test -f config/e-server.production.yaml
|
test -f config/e-server.production.yaml
|
||||||
test -f s-migrations.tar.gz
|
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
|
sha256sum -c SHA256SUMS >/dev/null
|
||||||
! rg -n '^([[:space:]]*)build:' compose
|
! rg -n '^([[:space:]]*)build:' compose
|
||||||
! rg -n 'overcooked-auth|overcooked-auth-webui' compose install.sh
|
! rg -n 'overcooked-auth|overcooked-auth-webui' compose install.sh
|
||||||
! rg -n 'S_TURNSTILE_(SITE|SECRET)_KEY: .*\\?required' compose
|
! 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 '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
|
rg -n 'control-proxy|PRODUCT_E_CONTROL_PLANE_TLS_' compose install.sh >/dev/null
|
||||||
|
./test/database-credentials.sh
|
||||||
echo 'static customer installer checks passed'
|
echo 'static customer installer checks passed'
|
||||||
Reference in new issue
Block a user