1
0
forked from baron/baron-sso

백업/복구로직 변경, 깜빡임 버그 해결

This commit is contained in:
2026-06-05 12:26:51 +09:00
parent 4bae1dd00d
commit 29038254dd
43 changed files with 3695 additions and 75 deletions

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() {
echo "ERROR: $*" >&2
exit 1
}
assert_dry_run_contains() {
local output="$1"
local expected="$2"
grep -Fq -- "$expected" <<<"$output" || fail "dry-run output must contain: $expected"
}
grep -Fq "FROM debian:trixie-slim" "$repo_root/docker/backup-tools/Dockerfile" \
|| fail "backup-tools image must be based on debian:trixie-slim."
grep -Fq "zstd" "$repo_root/docker/backup-tools/Dockerfile" \
|| fail "backup-tools image must include zstd so restore does not depend on the host."
grep -Fq "docker-cli" "$repo_root/docker/backup-tools/Dockerfile" \
|| fail "backup-tools image must include docker CLI for containerized dump/restore orchestration."
grep -Fq "perl" "$repo_root/docker/backup-tools/Dockerfile" \
|| fail "backup-tools image must include perl for legacy ClickHouse schema dump decoding."
dump_dry_run="$(
make --dry-run --always-make -C "$repo_root" dump DUMP_SERVICES="postgres,config" DUMP_MODE="maintenance" 2>&1
)"
assert_dry_run_contains "$dump_dry_run" "docker build"
assert_dry_run_contains "$dump_dry_run" "docker run"
assert_dry_run_contains "$dump_dry_run" "baron-sso-backup-tools:local"
assert_dry_run_contains "$dump_dry_run" "--env-file .env"
assert_dry_run_contains "$dump_dry_run" "/var/run/docker.sock:/var/run/docker.sock"
assert_dry_run_contains "$dump_dry_run" "/tmp:/tmp"
assert_dry_run_contains "$dump_dry_run" "scripts/backup/dump.sh"
assert_dry_run_contains "$dump_dry_run" "DUMP_SERVICES=\"postgres,config\""
assert_dry_run_contains "$dump_dry_run" "DUMP_MODE=\"maintenance\""
restore_dry_run="$(
make --dry-run --always-make -C "$repo_root" restore BACKUP="backups/example" DUMP_FILE="backups/example.tar.zst" RESTORE_SERVICES="postgres,config" CONFIRM_RESTORE="baron-sso" RESTORE_REPORT="reports/restore-report.json" 2>&1
)"
assert_dry_run_contains "$restore_dry_run" "scripts/backup/restore.sh"
assert_dry_run_contains "$restore_dry_run" "docker run"
assert_dry_run_contains "$restore_dry_run" "BACKUP=\"backups/example\""
assert_dry_run_contains "$restore_dry_run" "DUMP_FILE=\"backups/example.tar.zst\""
assert_dry_run_contains "$restore_dry_run" "RESTORE_SERVICES=\"postgres,config\""
assert_dry_run_contains "$restore_dry_run" "CONFIRM_RESTORE=\"baron-sso\""
assert_dry_run_contains "$restore_dry_run" "RESTORE_REPORT=\"reports/restore-report.json\""
for target in dump-verify restore-verify dump-list restore-plan; do
target_dry_run="$(
make --dry-run --always-make -C "$repo_root" "$target" BACKUP="backups/example" 2>&1
)"
assert_dry_run_contains "$target_dry_run" "scripts/backup/"
done
upload_dry_run="$(
make --dry-run --always-make -C "$repo_root" upload-cloud BACKUP="backups/example" WORKS_DRIVE_DRY_RUN=true 2>&1
)"
assert_dry_run_contains "$upload_dry_run" "docker run"
assert_dry_run_contains "$upload_dry_run" "scripts/backup/upload_cloud.sh"
assert_dry_run_contains "$upload_dry_run" "BACKUP=\"backups/example\""
assert_dry_run_contains "$upload_dry_run" "WORKS_DRIVE_DRY_RUN=\"true\""
if make -C "$repo_root" BACKUP_USE_DOCKER=false restore >/tmp/baron-sso-restore-missing.out 2>&1; then
fail "make restore must fail when BACKUP and CONFIRM_RESTORE are not provided."
fi
if ! grep -Fq "CONFIRM_RESTORE=baron-sso" /tmp/baron-sso-restore-missing.out; then
fail "make restore failure must mention the required confirmation value."
fi
echo "OK: backup Makefile targets expose the expected guarded interface"

View File

@@ -0,0 +1,135 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() {
echo "ERROR: $*" >&2
exit 1
}
source "$repo_root/scripts/backup/lib/common.sh"
source "$repo_root/scripts/backup/lib/manifest.sh"
source "$repo_root/scripts/backup/lib/report.sh"
assert_eq() {
local expected="$1"
local actual="$2"
local message="$3"
[[ "$actual" == "$expected" ]] || fail "$message: expected '$expected', got '$actual'"
}
all_services="$(normalize_service_filter "all")"
assert_eq "postgres ory-postgres clickhouse ory-clickhouse config" "$all_services" "all must expand to every supported service"
selected_services="$(normalize_service_filter "postgres,config")"
assert_eq "postgres config" "$selected_services" "comma-separated services must be normalized in input order"
ory_services="$(normalize_service_filter "ory-postgres,ory-clickhouse")"
assert_eq "ory-postgres ory-clickhouse" "$ory_services" "ory service filter must not expand to Baron services"
if service_enabled "postgres" "$ory_services"; then
fail "service_enabled must not match postgres inside ory-postgres"
fi
if service_enabled "clickhouse" "$ory_services"; then
fail "service_enabled must not match clickhouse inside ory-clickhouse"
fi
service_enabled "ory-postgres" "$ory_services" || fail "service_enabled must match exact ory-postgres token"
service_enabled "ory-clickhouse" "$ory_services" || fail "service_enabled must match exact ory-clickhouse token"
grep -Fq "drop database if exists" "$repo_root/scripts/backup/lib/clickhouse.sh" \
|| fail "ClickHouse restore must drop restored databases before replaying schema/data to avoid duplicate rows or init-table conflicts."
grep -Fq "FORMAT RawBLOB" "$repo_root/scripts/backup/lib/clickhouse.sh" \
|| fail "ClickHouse dump must store SHOW CREATE TABLE as raw SQL, not escaped TSV."
grep -Fq "render_clickhouse_schema" "$repo_root/scripts/backup/lib/clickhouse.sh" \
|| fail "ClickHouse restore must route schema files through the compatibility decoder."
grep -Fq "s{\\\\n}" "$repo_root/scripts/backup/lib/clickhouse.sh" \
|| fail "ClickHouse restore must decode escaped newlines from older schema dumps."
grep -Fq "x27" "$repo_root/scripts/backup/lib/clickhouse.sh" \
|| fail "ClickHouse restore must decode escaped single quotes from older schema dumps."
grep -Fq "filter_clickhouse_stable_row_counts" "$repo_root/scripts/backup/restore.sh" \
|| fail "restore verification must not fail on unstable ClickHouse aggregate/view physical row counts."
grep -Fq "collect_clickhouse_native_stable_row_counts" "$repo_root/scripts/backup/restore.sh" \
|| fail "restore verification must derive ClickHouse expected counts from Native data files."
if normalize_service_filter "postgres,unknown" >/tmp/baron-sso-service-filter.out 2>&1; then
fail "unknown backup service must be rejected"
fi
if ! grep -Fq "unknown backup service" /tmp/baron-sso-service-filter.out; then
fail "unknown service rejection must explain the service filter problem"
fi
tmp_dir="$(mktemp -d /tmp/baron-sso-backup-policy.XXXXXX)"
trap 'rm -rf "$tmp_dir"' EXIT INT TERM
mkdir -p "$tmp_dir/reports"
create_manifest "$tmp_dir" "maintenance" "postgres config"
manifest="$tmp_dir/manifest.json"
[[ -f "$manifest" ]] || fail "create_manifest must write manifest.json"
grep -Fq '"format_version": "1"' "$manifest" || fail "manifest must include format_version"
grep -Fq '"mode": "maintenance"' "$manifest" || fail "manifest must include backup mode"
grep -Fq '"services": [' "$manifest" || fail "manifest must include service list"
grep -Fq '"git_commit":' "$manifest" || fail "manifest must include git commit"
cat >"$tmp_dir/reports/baron-postgres-row-counts.txt" <<'EOF'
public.users:228
public.tenants:266
public.relying_parties:1
EOF
cat >"$tmp_dir/reports/ory_hydra-row-counts.txt" <<'EOF'
public.hydra_client:5
EOF
write_backup_markdown_report "$tmp_dir" "succeeded" "postgres config" '[{"service":"postgres","duration_seconds":2},{"service":"config","duration_seconds":1}]'
backup_md="$tmp_dir/reports/backup-report.md"
[[ -f "$backup_md" ]] || fail "backup markdown report must be created."
grep -Fq "# Baron SSO Backup Report" "$backup_md" || fail "backup report must have a markdown title."
grep -Fq "| 사용자 | 228 |" "$backup_md" || fail "backup report must include user count."
grep -Fq "| 테넌트 | 266 |" "$backup_md" || fail "backup report must include tenant count."
grep -Fq "| RP | 1 |" "$backup_md" || fail "backup report must include RP count."
grep -Fq "| postgres | 2 |" "$backup_md" || fail "backup report must include service duration."
printf 'original\n' >"$tmp_dir/example.txt"
(cd "$tmp_dir" && sha256sum example.txt > checksums.sha256)
printf 'changed\n' >"$tmp_dir/example.txt"
if BACKUP="$tmp_dir" "$repo_root/scripts/backup/verify-dump.sh" >/tmp/baron-sso-checksum.out 2>&1; then
fail "verify-dump must fail on checksum mismatch"
fi
if ! grep -Fq "checksum verification failed" /tmp/baron-sso-checksum.out; then
fail "checksum mismatch output must be explicit"
fi
if BACKUP="$tmp_dir" CONFIRM_RESTORE="baron-sso" RESTORE_TEST_NON_EMPTY=1 "$repo_root/scripts/backup/restore.sh" --dry-run >/tmp/baron-sso-non-empty-restore.out 2>&1; then
fail "restore must reject non-empty targets by default"
fi
if ! grep -Fq "non-empty restore target is not allowed" /tmp/baron-sso-non-empty-restore.out; then
fail "restore must explain the non-empty target guard"
fi
archive_source="$tmp_dir/archive-source"
mkdir -p "$archive_source/reports"
create_manifest "$archive_source" "maintenance" "postgres"
printf 'archive fixture\n' >"$archive_source/example.txt"
(cd "$archive_source" && sha256sum manifest.json example.txt > checksums.sha256)
archive_file="$tmp_dir/archive-source.tar.zst"
tar --zstd -cf "$archive_file" -C "$tmp_dir" archive-source
restore_report="$tmp_dir/restore-report.json"
DUMP_FILE="$archive_file" \
CONFIRM_RESTORE="baron-sso" \
RESTORE_REPORT="$restore_report" \
"$repo_root/scripts/backup/restore.sh" --dry-run >/tmp/baron-sso-dump-file-restore.out
[[ -f "$restore_report" ]] || fail "restore dry-run with DUMP_FILE must write RESTORE_REPORT."
jq -e \
--arg dump_file "$archive_file" \
'.status == "planned" and .dump_file == $dump_file and .backup_dir != null and (.services | index("postgres"))' \
"$restore_report" >/dev/null || fail "restore report must describe planned DUMP_FILE restore."
echo "OK: backup scripts enforce parser, manifest, checksum, and restore safety policies"

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail() {
echo "ERROR: $*" >&2
exit 1
}
tmp_dir="$(mktemp -d /tmp/baron-sso-upload-cloud-test.XXXXXX)"
trap 'rm -rf "$tmp_dir"' EXIT INT TERM
backup_dir="$tmp_dir/baron-sso-backup-20260605-000000Z"
mkdir -p "$backup_dir/postgres" "$backup_dir/reports"
printf '{"format_version":"1"}\n' >"$backup_dir/manifest.json"
printf 'postgres dump fixture\n' >"$backup_dir/postgres/baron.dump"
printf '# Baron SSO Backup Report\n' >"$backup_dir/reports/backup-report.md"
(cd "$backup_dir" && sha256sum manifest.json postgres/baron.dump > checksums.sha256)
if "$repo_root/scripts/backup/upload_cloud.sh" >/tmp/baron-sso-upload-missing.out 2>&1; then
fail "upload_cloud.sh must require BACKUP."
fi
if ! grep -Fq "BACKUP is required" /tmp/baron-sso-upload-missing.out; then
fail "missing BACKUP error must be explicit."
fi
curl_log="$tmp_dir/curl.log"
fake_curl="$tmp_dir/fake-curl.sh"
fake_bin="$tmp_dir/bin"
mkdir -p "$fake_bin"
cat >"$fake_curl" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >>"${FAKE_CURL_LOG}"
last_arg="${!#}"
case "$last_arg" in
https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/folder-1/children)
printf '{"files":[]}'
;;
https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/folder-1/createfolder)
printf '{"fileId":"reports-folder-1","fileName":"reports","fileType":"FOLDER"}'
;;
https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/folder-1)
printf '{"uploadUrl":"https://upload.example.test/upload-1"}'
;;
https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/reports-folder-1)
printf '{"uploadUrl":"https://upload.example.test/upload-report-1"}'
;;
https://upload.example.test/upload-1)
printf '{"fileId":"file-1"}'
;;
https://upload.example.test/upload-report-1)
printf '{"fileId":"report-file-1"}'
;;
*)
echo "unexpected curl URL: $last_arg" >&2
exit 2
;;
esac
EOF
chmod +x "$fake_curl"
cat >"$fake_bin/zstd" <<'EOF'
#!/usr/bin/env bash
cat
EOF
chmod +x "$fake_bin/zstd"
WORKS_DRIVE_ACCESS_TOKEN="test-access-token" \
WORKS_DRIVE_TARGET="sharedrive" \
WORKS_DRIVE_SHARED_DRIVE_ID="shared-drive-1" \
WORKS_DRIVE_PARENT_FILE_ID="folder-1" \
WORKS_DRIVE_CURL_BIN="$fake_curl" \
FAKE_CURL_LOG="$curl_log" \
PATH="$fake_bin:$PATH" \
BACKUP="$backup_dir" \
"$repo_root/scripts/backup/upload_cloud.sh" >"$tmp_dir/upload.out"
grep -Fq "Upload complete" "$tmp_dir/upload.out" || fail "upload must complete with fake curl."
grep -Fq "sharedrives/shared-drive-1/files/folder-1" "$curl_log" || fail "must create upload URL for the configured shared drive folder."
grep -Fq "https://upload.example.test/upload-1" "$curl_log" || fail "must upload to the issued upload URL."
grep -Fq "Authorization: Bearer test-access-token" "$curl_log" || fail "must pass bearer token to WORKS API calls."
grep -Fq "Filedata=@" "$curl_log" || fail "must upload the packaged backup as multipart Filedata."
grep -Fq ".tar.zst" "$curl_log" || fail "backup directory uploads must be packaged as .tar.zst."
grep -Fq "createfolder" "$curl_log" || fail "must create or resolve a report subfolder."
grep -Fq "reports-folder-1" "$curl_log" || fail "must upload markdown reports to the reports folder."
grep -Eq "backup-report-[0-9]{8}-[0-9]{6}Z.md" "$curl_log" || fail "must upload timestamped backup markdown report."
if grep -Fq "cloud-upload.json" "$curl_log"; then
fail "cloud-upload.json must not be uploaded to WORKS Drive."
fi
report_file="$backup_dir/reports/cloud-upload.json"
[[ -f "$report_file" ]] || fail "upload must write reports/cloud-upload.json."
jq -e '.target == "sharedrive" and .files[0].status == "uploaded" and .report_files[0].status == "uploaded" and (.report_files[0].file_name | test("^backup-report-[0-9]{8}-[0-9]{6}Z[.]md$"))' "$report_file" >/dev/null || fail "upload report must include timestamped markdown report file status."
WORKS_DRIVE_DRY_RUN=true \
WORKS_DRIVE_TARGET="sharedrive" \
WORKS_DRIVE_SHARED_DRIVE_ID="shared-drive-1" \
WORKS_DRIVE_PARENT_FILE_ID="folder-1" \
PATH="$fake_bin:$PATH" \
BACKUP="$backup_dir" \
"$repo_root/scripts/backup/upload_cloud.sh" >"$tmp_dir/dry-run.out"
grep -Fq "Dry run" "$tmp_dir/dry-run.out" || fail "dry-run must not require a token or call curl."
echo "OK: upload_cloud uploads current backup artifacts to WORKS Drive"

View File

@@ -11,7 +11,7 @@ docker_config="$(
)"
override_env="$(mktemp)"
cp "$repo_root/.env" "$override_env"
grep -Ev '^(USERFRONT_URL|HYDRA_PUBLIC_URL|KRATOS_UI_URL|KRATOS_BROWSER_URL|ADMINFRONT_CALLBACK_URLS|DEVFRONT_CALLBACK_URLS|ORGFRONT_CALLBACK_URLS)=' "$repo_root/.env" >"$override_env"
cat >> "$override_env" <<'EOF'
USERFRONT_URL=https://compose-policy.example.test/sso
HYDRA_PUBLIC_URL=https://compose-policy.example.test/sso/oidc
@@ -112,8 +112,28 @@ root_init_rp="$(
docker_init_rp="$(
awk 'in_block && /^ [A-Za-z0-9_-]+:/ { exit } /^ init-rp:/ { in_block=1 } in_block { print }' "$repo_root/docker/compose.ory.yaml"
)"
if grep -q "image: oryd/hydra" <<<"$root_init_rp$docker_init_rp"; then
echo "ERROR: init-rp must not use the Hydra service image because distroless tags do not provide /bin/sh." >&2
for init_rp_file in \
"$repo_root/compose.ory.yaml" \
"$repo_root/docker/compose.ory.yaml" \
"$repo_root/docker/staging_pull_compose.template.yaml" \
"$repo_root/deploy/templates/docker-compose.yaml"
do
if ! grep -Fq 'image: oryd/hydra:${HYDRA_CLI_VERSION:-v26.2.0}' "$init_rp_file"; then
echo "ERROR: init-rp must use the official Hydra CLI image with HYDRA_CLI_VERSION in $init_rp_file." >&2
exit 1
fi
if ! grep -Fq 'entrypoint: ["/bin/sh", "-ec"]' "$init_rp_file"; then
echo "ERROR: init-rp must override the Hydra image entrypoint with /bin/sh -ec in $init_rp_file." >&2
exit 1
fi
if grep -Fq 'HYDRA_CLI_ARCHIVE_VERSION' "$init_rp_file" || grep -Fq 'hydra.tar.gz' "$init_rp_file"; then
echo "ERROR: init-rp must not download Hydra CLI tarballs at runtime in $init_rp_file." >&2
exit 1
fi
done
if grep -q "image: alpine:latest" <<<"$root_init_rp$docker_init_rp"; then
echo "ERROR: init-rp must not use alpine plus runtime Hydra CLI download." >&2
exit 1
fi