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,115 @@
#!/usr/bin/env bash
clickhouse_query() {
local container="$1"
local user="$2"
local password="$3"
local query="$4"
docker exec "$container" clickhouse-client --user "$user" --password "$password" --query "$query"
}
render_clickhouse_schema() {
local schema_file="$1"
if head -n 1 "$schema_file" | grep -q '\\n'; then
perl -0pe 's{\\n}{\n}g; s{\\t}{\t}g; s{\\\x27}{\x27}g' "$schema_file"
return
fi
cat "$schema_file"
}
dump_clickhouse_container() {
local backup_dir="$1"
local service_name="$2"
local container="$3"
local user="$4"
local password="$5"
local output_dir="$backup_dir/clickhouse/$service_name"
local table_list="$output_dir/tables.tsv"
local database
local table
local engine
local safe_name
backup_require_command docker
backup_require_container "$container"
mkdir -p "$output_dir/schema" "$output_dir/data" "$backup_dir/reports"
backup_log "Dumping ClickHouse metadata and Native data: $container"
clickhouse_query "$container" "$user" "$password" \
"select database, name, engine from system.tables where database not in ('INFORMATION_SCHEMA','information_schema','system') order by database, if(positionCaseInsensitive(engine, 'View') > 0, 1, 0), name format TSV" \
>"$table_list"
while IFS=$'\t' read -r database table engine; do
[[ -n "$database" && -n "$table" ]] || continue
safe_name="${database}__${table}"
clickhouse_query "$container" "$user" "$password" "show create table \`${database}\`.\`${table}\` FORMAT RawBLOB" >"$output_dir/schema/${safe_name}.sql"
if [[ "$engine" != *View* ]]; then
clickhouse_query "$container" "$user" "$password" "select * from \`${database}\`.\`${table}\` format Native" >"$output_dir/data/${safe_name}.native"
fi
clickhouse_query "$container" "$user" "$password" "select '${database}.${table}:' || toString(count()) from \`${database}\`.\`${table}\`" \
>>"$backup_dir/reports/${service_name}-row-counts.txt"
done <"$table_list"
}
restore_clickhouse_container() {
local backup_dir="$1"
local service_name="$2"
local container="$3"
local user="$4"
local password="$5"
local input_dir="$backup_dir/clickhouse/$service_name"
local table_list="$input_dir/tables.tsv"
local database
local table
local engine
local safe_name
local restored_databases=""
backup_require_command docker
backup_require_container "$container"
backup_require_path "$table_list"
backup_log "Restoring ClickHouse tables: $container"
while IFS=$'\t' read -r database table engine; do
[[ -n "$database" && -n "$table" ]] || continue
if ! grep -qw -- "$database" <<<"$restored_databases"; then
docker exec "$container" clickhouse-client --user "$user" --password "$password" \
--query "drop database if exists \`${database}\`"
docker exec "$container" clickhouse-client --user "$user" --password "$password" \
--query "create database if not exists \`${database}\`"
restored_databases="${restored_databases:+$restored_databases }$database"
fi
done <"$table_list"
while IFS=$'\t' read -r database table engine; do
[[ -n "$database" && -n "$table" ]] || continue
safe_name="${database}__${table}"
backup_require_path "$input_dir/schema/${safe_name}.sql"
render_clickhouse_schema "$input_dir/schema/${safe_name}.sql" \
| docker exec -i "$container" clickhouse-client --user "$user" --password "$password" --multiquery
if [[ "$engine" != *View* ]]; then
backup_require_path "$input_dir/data/${safe_name}.native"
docker exec -i "$container" clickhouse-client --user "$user" --password "$password" \
--query "insert into \`${database}\`.\`${table}\` format Native" <"$input_dir/data/${safe_name}.native"
fi
done <"$table_list"
}
dump_baron_clickhouse() {
dump_clickhouse_container "$1" "baron_clickhouse" "baron_clickhouse" "${CLICKHOUSE_USER:-baron}" "${CLICKHOUSE_PASSWORD:-password}"
}
dump_ory_clickhouse() {
dump_clickhouse_container "$1" "ory_clickhouse" "ory_clickhouse" "${ORY_CLICKHOUSE_USER:-ory}" "${ORY_CLICKHOUSE_PASSWORD:-orypass}"
}
restore_baron_clickhouse() {
restore_clickhouse_container "$1" "baron_clickhouse" "baron_clickhouse" "${CLICKHOUSE_USER:-baron}" "${CLICKHOUSE_PASSWORD:-password}"
}
restore_ory_clickhouse() {
restore_clickhouse_container "$1" "ory_clickhouse" "ory_clickhouse" "${ORY_CLICKHOUSE_USER:-ory}" "${ORY_CLICKHOUSE_PASSWORD:-orypass}"
}

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env bash
BACKUP_SUPPORTED_SERVICES="postgres ory-postgres clickhouse ory-clickhouse config"
backup_repo_root() {
if [[ -n "${BACKUP_REPO_ROOT:-}" ]]; then
printf '%s\n' "$BACKUP_REPO_ROOT"
return
fi
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
printf '%s\n' "$script_dir"
}
backup_log() {
printf '==> %s\n' "$*"
}
backup_die() {
printf 'ERROR: %s\n' "$*" >&2
return 1
}
backup_require_command() {
local command_name="$1"
command -v "$command_name" >/dev/null 2>&1 || backup_die "required command not found: $command_name"
}
backup_utc_now() {
date -u '+%Y-%m-%dT%H:%M:%SZ'
}
backup_timestamp() {
date -u '+%Y%m%d-%H%M%SZ'
}
backup_git_commit() {
local repo_root="$1"
git -c "safe.directory=$repo_root" -C "$repo_root" rev-parse --short=12 HEAD 2>/dev/null || printf 'unknown'
}
normalize_service_filter() {
local raw="${1:-all}"
local normalized=""
local candidate
if [[ "$raw" == "all" || -z "$raw" ]]; then
printf '%s\n' "$BACKUP_SUPPORTED_SERVICES"
return
fi
raw="${raw//,/ }"
for candidate in $raw; do
if ! grep -qw -- "$candidate" <<<"$BACKUP_SUPPORTED_SERVICES"; then
backup_die "unknown backup service: $candidate"
return 1
fi
if ! grep -qw -- "$candidate" <<<"$normalized"; then
normalized="${normalized:+$normalized }$candidate"
fi
done
[[ -n "$normalized" ]] || backup_die "service filter must not be empty"
printf '%s\n' "$normalized"
}
service_enabled() {
local service="$1"
local services="$2"
local candidate
for candidate in $services; do
[[ "$candidate" == "$service" ]] && return 0
done
return 1
}
backup_require_path() {
local path="$1"
[[ -e "$path" ]] || backup_die "required path not found: $path"
}
backup_container_running() {
local container="$1"
docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null | grep -qx 'true'
}
backup_require_container() {
local container="$1"
backup_container_running "$container" || backup_die "container is not running: $container"
}
backup_redact_env() {
local source_file="$1"
local target_file="$2"
sed -E '/^[[:space:]]*#/! s/^([^=]*(SECRET|PASSWORD|TOKEN|KEY|PRIVATE|CLIENT_SECRET|COOKIE)[^=]*)=.*/\1=REDACTED/I' "$source_file" >"$target_file"
}
backup_checksum_file() {
local backup_dir="$1"
(
cd "$backup_dir"
find . -type f ! -name 'checksums.sha256' -print0 \
| sort -z \
| xargs -0 sha256sum
) >"$backup_dir/checksums.sha256"
}
backup_verify_checksums() {
local backup_dir="$1"
backup_require_path "$backup_dir/checksums.sha256"
(
cd "$backup_dir"
sha256sum -c checksums.sha256
)
}
backup_safe_tar() {
local source_path="$1"
local target_base="$2"
local source_parent
local source_name
[[ -e "$source_path" ]] || return 0
source_parent="$(dirname "$source_path")"
source_name="$(basename "$source_path")"
if command -v zstd >/dev/null 2>&1; then
tar --zstd -cf "${target_base}.tar.zst" -C "$source_parent" "$source_name"
else
tar -czf "${target_base}.tar.gz" -C "$source_parent" "$source_name"
fi
}

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
dump_config_snapshot() {
local backup_dir="$1"
local repo_root
repo_root="$(backup_repo_root)"
mkdir -p "$backup_dir/config"
if [[ -f "$repo_root/.env" ]]; then
backup_redact_env "$repo_root/.env" "$backup_dir/config/env.redacted"
fi
backup_safe_tar "$repo_root/config/.generated/ory" "$backup_dir/config/generated-ory"
backup_safe_tar "$repo_root/gateway" "$backup_dir/config/gateway"
mkdir -p "$backup_dir/config/compose"
for compose_file in compose.infra.yaml compose.ory.yaml docker-compose.yaml; do
if [[ -f "$repo_root/$compose_file" ]]; then
cp "$repo_root/$compose_file" "$backup_dir/config/compose/$compose_file"
fi
done
}
restore_config_snapshot() {
local backup_dir="$1"
local repo_root
local output_dir
repo_root="$(backup_repo_root)"
output_dir="$repo_root/config-restored"
backup_require_path "$backup_dir/config"
mkdir -p "$output_dir"
cp -R "$backup_dir/config/." "$output_dir/"
backup_log "Config snapshot was copied to $output_dir for manual review."
}

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env bash
create_manifest() {
local backup_dir="$1"
local mode="$2"
local services="$3"
local repo_root
local created_at
local git_commit
local service
local first=1
repo_root="$(backup_repo_root)"
created_at="$(backup_utc_now)"
git_commit="$(backup_git_commit "$repo_root")"
{
printf '{\n'
printf ' "format_version": "1",\n'
printf ' "created_at": "%s",\n' "$created_at"
printf ' "git_commit": "%s",\n' "$git_commit"
printf ' "mode": "%s",\n' "$mode"
printf ' "environment_scope": "same-env-only",\n'
printf ' "services": ['
for service in $services; do
if [[ "$first" -eq 1 ]]; then
first=0
else
printf ', '
fi
printf '"%s"' "$service"
done
printf '],\n'
printf ' "restore_policy": {\n'
printf ' "requires_empty_target": true,\n'
printf ' "requires_confirmation": "baron-sso",\n'
printf ' "auto_run_migrations": false,\n'
printf ' "works_relay_auto_resume": false\n'
printf ' }\n'
printf '}\n'
} >"$backup_dir/manifest.json"
}

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env bash
dump_baron_postgres() {
local backup_dir="$1"
local db_user="${DB_USER:-baron}"
local db_password="${DB_PASSWORD:-password}"
local db_name="${DB_NAME:-baron_sso}"
backup_require_command docker
backup_require_container baron_postgres
mkdir -p "$backup_dir/postgres" "$backup_dir/reports"
backup_log "Dumping Baron Postgres database: $db_name"
docker exec -e "PGPASSWORD=$db_password" baron_postgres \
pg_dump -U "$db_user" -d "$db_name" -Fc >"$backup_dir/postgres/baron.dump"
docker exec -e "PGPASSWORD=$db_password" baron_postgres \
psql -U "$db_user" -d "$db_name" -Atc "select schemaname || '.' || relname || ':' || (xpath('/row/c/text()', query_to_xml(format('select count(*) as c from %I.%I', schemaname, relname), false, true, '')))[1]::text from pg_stat_user_tables order by 1" \
>"$backup_dir/reports/baron-postgres-row-counts.txt"
}
dump_ory_postgres() {
local backup_dir="$1"
local db_user="${ORY_POSTGRES_USER:-ory}"
local db_password="${ORY_POSTGRES_PASSWORD:-secret}"
local kratos_db="${KRATOS_DB:-ory_kratos}"
local hydra_db="${HYDRA_DB:-ory_hydra}"
local keto_db="${KETO_DB:-ory_keto}"
local db_name
backup_require_command docker
backup_require_container ory_postgres
mkdir -p "$backup_dir/postgres" "$backup_dir/reports"
backup_log "Dumping Ory Postgres globals"
docker exec -e "PGPASSWORD=$db_password" ory_postgres \
pg_dumpall -U "$db_user" --globals-only >"$backup_dir/postgres/globals.sql"
for db_name in "$kratos_db" "$hydra_db" "$keto_db"; do
backup_log "Dumping Ory Postgres database: $db_name"
docker exec -e "PGPASSWORD=$db_password" ory_postgres \
pg_dump -U "$db_user" -d "$db_name" -Fc >"$backup_dir/postgres/${db_name}.dump"
docker exec -e "PGPASSWORD=$db_password" ory_postgres \
psql -U "$db_user" -d "$db_name" -Atc "select schemaname || '.' || relname || ':' || (xpath('/row/c/text()', query_to_xml(format('select count(*) as c from %I.%I', schemaname, relname), false, true, '')))[1]::text from pg_stat_user_tables order by 1" \
>"$backup_dir/reports/${db_name}-row-counts.txt"
done
}
restore_baron_postgres() {
local backup_dir="$1"
local db_user="${DB_USER:-baron}"
local db_password="${DB_PASSWORD:-password}"
local db_name="${DB_NAME:-baron_sso}"
backup_require_path "$backup_dir/postgres/baron.dump"
backup_require_command docker
backup_require_container baron_postgres
backup_log "Restoring Baron Postgres database: $db_name"
docker exec -i -e "PGPASSWORD=$db_password" baron_postgres \
pg_restore -U "$db_user" -d "$db_name" --clean --if-exists <"$backup_dir/postgres/baron.dump"
}
restore_ory_postgres() {
local backup_dir="$1"
local db_user="${ORY_POSTGRES_USER:-ory}"
local db_password="${ORY_POSTGRES_PASSWORD:-secret}"
local kratos_db="${KRATOS_DB:-ory_kratos}"
local hydra_db="${HYDRA_DB:-ory_hydra}"
local keto_db="${KETO_DB:-ory_keto}"
local db_name
backup_require_command docker
backup_require_container ory_postgres
for db_name in "$kratos_db" "$hydra_db" "$keto_db"; do
backup_require_path "$backup_dir/postgres/${db_name}.dump"
backup_log "Restoring Ory Postgres database: $db_name"
docker exec -i -e "PGPASSWORD=$db_password" ory_postgres \
pg_restore -U "$db_user" -d "$db_name" --clean --if-exists <"$backup_dir/postgres/${db_name}.dump"
done
}
postgres_target_has_data() {
local container="$1"
local user="$2"
local password="$3"
local database="$4"
backup_require_command docker
backup_require_container "$container"
docker exec -e "PGPASSWORD=$password" "$container" \
psql -U "$user" -d "$database" -Atc "select exists (select 1 from pg_tables where schemaname not in ('pg_catalog','information_schema') limit 1)" \
2>/dev/null | grep -qx 't'
}

View File

@@ -0,0 +1,142 @@
#!/usr/bin/env bash
report_count_from_file() {
local file_path="$1"
local key="$2"
if [[ ! -f "$file_path" ]]; then
printf '0\n'
return
fi
awk -F: -v key="$key" '$1 == key {print $2; found=1; exit} END {if (!found) print "0"}' "$file_path"
}
report_first_count() {
local key="$1"
shift
local file_path
local count
for file_path in "$@"; do
count="$(report_count_from_file "$file_path" "$key")"
if [[ "$count" != "0" ]]; then
printf '%s\n' "$count"
return
fi
done
printf '0\n'
}
write_backup_markdown_report() {
local backup_dir="$1"
local status="$2"
local services="$3"
local timings_json="${4:-[]}"
local reports_dir="$backup_dir/reports"
local output_file="$reports_dir/backup-report.md"
local created_at
local manifest_created_at="unknown"
local git_commit="unknown"
local users
local tenants
local relying_parties
local hydra_clients
local works_org_units
local works_users
local timings_table
mkdir -p "$reports_dir"
created_at="$(backup_utc_now)"
if [[ -f "$backup_dir/manifest.json" ]]; then
manifest_created_at="$(jq -r '.created_at // "unknown"' "$backup_dir/manifest.json")"
git_commit="$(jq -r '.git_commit // "unknown"' "$backup_dir/manifest.json")"
fi
users="$(report_first_count "public.users" "$reports_dir/baron-postgres-row-counts.txt")"
tenants="$(report_first_count "public.tenants" "$reports_dir/baron-postgres-row-counts.txt")"
relying_parties="$(report_first_count "public.relying_parties" "$reports_dir/baron-postgres-row-counts.txt")"
hydra_clients="$(report_first_count "public.hydra_client" "$reports_dir/ory_hydra-row-counts.txt")"
works_org_units="$(report_first_count "public.works_org_units" "$reports_dir/baron-postgres-row-counts.txt")"
works_users="$(report_first_count "public.works_users" "$reports_dir/baron-postgres-row-counts.txt")"
timings_table="$(jq -r '
if type != "array" or length == 0 then
"| 없음 | 0 |"
else
.[] | "| \(.service) | \(.duration_seconds) |"
end
' <<<"$timings_json")"
{
printf '# Baron SSO Backup Report\n\n'
printf '| 항목 | 값 |\n'
printf '| --- | --- |\n'
printf '| 생성 시각 | %s |\n' "$created_at"
printf '| 백업 시각 | %s |\n' "$manifest_created_at"
printf '| 상태 | %s |\n' "$status"
printf '| 백업 경로 | `%s` |\n' "$backup_dir"
printf '| Git Commit | `%s` |\n' "$git_commit"
printf '| 서비스 | `%s` |\n\n' "$services"
printf '## 요약\n\n'
printf '| 지표 | 값 |\n'
printf '| --- | ---: |\n'
printf '| 사용자 | %s |\n' "$users"
printf '| 테넌트 | %s |\n' "$tenants"
printf '| RP | %s |\n' "$relying_parties"
printf '| Hydra Client | %s |\n' "$hydra_clients"
printf '| WORKS 조직 | %s |\n' "$works_org_units"
printf '| WORKS 사용자 | %s |\n\n' "$works_users"
printf '## 서비스별 수행 시간\n\n'
printf '| 서비스 | 초 |\n'
printf '| --- | ---: |\n'
printf '%s\n' "$timings_table"
} >"$output_file"
}
write_restore_markdown_report() {
local restore_json="$1"
local output_file
[[ -f "$restore_json" ]] || return 0
output_file="${restore_json%.json}.md"
jq -r '
def services: (.services // [] | join(", "));
def verification_rows:
(.verification.target_reports // []) as $reports
| if ($reports | length) == 0 then
"| 없음 | not_run | |"
else
$reports[]
| "| \(.service) | \(.status) | \(.diff_file // "") |"
end;
"# Baron SSO Restore Report\n",
"| 항목 | 값 |",
"| --- | --- |",
"| 시작 시각 | \(.started_at // "unknown") |",
"| 종료 시각 | \(.finished_at // "unknown") |",
"| 상태 | \(.status // "unknown") |",
"| 메시지 | \(.message // "") |",
"| 입력 유형 | \(.backup_source // "unknown") |",
"| 백업 경로 | `\(.backup_dir // "")` |",
"| Dump 파일 | `\(.dump_file // "")` |",
"| 서비스 | `\(services)` |",
"",
"## 검증",
"",
"| 항목 | 상태 |",
"| --- | --- |",
"| Dump checksum | \(.verification.dump_checksum // "not_run") |",
"| 대상 row count | \(.verification.target_row_counts // "not_run") |",
"",
"## 대상별 검증 결과",
"",
"| 서비스 | 상태 | Diff |",
"| --- | --- | --- |",
verification_rows
' "$restore_json" >"$output_file"
}