From efe1ba0505b9081a876db09b67ef7099e32960e9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 18 Jul 2026 17:23:50 +0800 Subject: [PATCH] fix(backup): bound cron backup containers --- scripts/backup/backup-clawbot.sh | 2 +- scripts/backup/common.sh | 5 +++ ...test_backup_container_resource_contract.py | 35 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 scripts/backup/tests/test_backup_container_resource_contract.py diff --git a/scripts/backup/backup-clawbot.sh b/scripts/backup/backup-clawbot.sh index 7e38d0c62..10738211d 100755 --- a/scripts/backup/backup-clawbot.sh +++ b/scripts/backup/backup-clawbot.sh @@ -33,7 +33,7 @@ main() { # Step 2: SSH 到 188 將 Redis volume 打包傳回 log_info "從 192.168.0.188 拉取 clawbot-redis volume..." - if ssh "${REMOTE_HOST}" "docker run --rm -v clawbot-v5_clawbot-redis-data:/data alpine tar czf - /data 2>/dev/null" > "${DUMP_DIR}/clawbot-redis_${timestamp}.tar.gz"; then + if ssh "${REMOTE_HOST}" "docker run --rm --cpus=$(printf '%q' "${BACKUP_DOCKER_CPUS}") --memory=$(printf '%q' "${BACKUP_DOCKER_MEMORY}") --memory-swap=$(printf '%q' "${BACKUP_DOCKER_MEMORY_SWAP}") -v clawbot-v5_clawbot-redis-data:/data:ro alpine tar czf - /data 2>/dev/null" > "${DUMP_DIR}/clawbot-redis_${timestamp}.tar.gz"; then local size=$(du -h "${DUMP_DIR}/clawbot-redis_${timestamp}.tar.gz" | cut -f1) log_success "ClawBot Redis volume 拉取完成 (${size})" else diff --git a/scripts/backup/common.sh b/scripts/backup/common.sh index ad5cda727..69c0e4e87 100644 --- a/scripts/backup/common.sh +++ b/scripts/backup/common.sh @@ -25,6 +25,11 @@ export KEEP_DAILY=30 # 2026-04-05 Claude Code: 延長保留 (原7→30) export KEEP_WEEKLY=12 # 2026-04-05 Claude Code: 延長保留 (原4→12) export KEEP_MONTHLY=24 # 2026-04-05 Claude Code: 延長保留 (原6→24) +# Ephemeral backup containers must remain bounded even when cron has no env file. +export BACKUP_DOCKER_CPUS="${BACKUP_DOCKER_CPUS:-1.0}" +export BACKUP_DOCKER_MEMORY="${BACKUP_DOCKER_MEMORY:-1g}" +export BACKUP_DOCKER_MEMORY_SWAP="${BACKUP_DOCKER_MEMORY_SWAP:-1g}" + # ----------------------------------------------------------------------------- # 日誌函式 # ----------------------------------------------------------------------------- diff --git a/scripts/backup/tests/test_backup_container_resource_contract.py b/scripts/backup/tests/test_backup_container_resource_contract.py new file mode 100644 index 000000000..8f908c294 --- /dev/null +++ b/scripts/backup/tests/test_backup_container_resource_contract.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[3] +COMMON = ROOT / "scripts" / "backup" / "common.sh" +CLAWBOT = ROOT / "scripts" / "backup" / "backup-clawbot.sh" +SENTRY = ROOT / "scripts" / "backup" / "backup-sentry.sh" + + +def test_common_defines_bounded_container_defaults_for_cron() -> None: + source = COMMON.read_text(encoding="utf-8") + + assert 'BACKUP_DOCKER_CPUS="${BACKUP_DOCKER_CPUS:-1.0}"' in source + assert 'BACKUP_DOCKER_MEMORY="${BACKUP_DOCKER_MEMORY:-1g}"' in source + assert 'BACKUP_DOCKER_MEMORY_SWAP="${BACKUP_DOCKER_MEMORY_SWAP:-1g}"' in source + + +def test_sentry_and_clawbot_backup_containers_use_the_bounded_contract() -> None: + common = COMMON.read_text(encoding="utf-8") + for script in (CLAWBOT, SENTRY): + source = script.read_text(encoding="utf-8") + + assert 'source "$(dirname "$0")/common.sh"' in source + for variable in ( + "BACKUP_DOCKER_CPUS", + "BACKUP_DOCKER_MEMORY", + "BACKUP_DOCKER_MEMORY_SWAP", + ): + assert variable in common + assert variable in source + + clawbot = CLAWBOT.read_text(encoding="utf-8") + assert "clawbot-v5_clawbot-redis-data:/data:ro" in clawbot