feat(observability): close native SigNoz backup restore canary
This commit is contained in:
@@ -5,6 +5,7 @@ import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SCRIPT = ROOT / "scripts" / "backup" / "backup-signoz.sh"
|
||||
BACKUP_ALL = ROOT / "scripts" / "backup" / "backup-all.sh"
|
||||
@@ -20,27 +21,24 @@ def _run_backup(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
initial_running: bool = True,
|
||||
docker_run_mode: str = "success",
|
||||
docker_run_target: str = "signoz-clickhouse",
|
||||
archive_timeout_target: str = "",
|
||||
tar_verify_mode: str = "success",
|
||||
tar_verify_target: str = "clickhouse_",
|
||||
tar_timeout_target: str = "",
|
||||
docker_start_failures: int = 0,
|
||||
native_check_status: int = 0,
|
||||
native_apply_status: int = 0,
|
||||
continuity_mode: str = "stable",
|
||||
restic_backup_status: int = 0,
|
||||
restore_max_attempts: int = 3,
|
||||
skip_retention: bool = False,
|
||||
) -> tuple[subprocess.CompletedProcess[str], list[str], Path]:
|
||||
harness = tmp_path / "backup"
|
||||
fake_bin = tmp_path / "bin"
|
||||
backup_base = tmp_path / "repository"
|
||||
event_log = tmp_path / "events.log"
|
||||
collector_id = tmp_path / "collector.id"
|
||||
collector_state = tmp_path / "collector.state"
|
||||
start_count = tmp_path / "start.count"
|
||||
harness.mkdir()
|
||||
fake_bin.mkdir()
|
||||
(backup_base / "signoz" / "data").mkdir(parents=True)
|
||||
shutil.copy2(SCRIPT, harness / SCRIPT.name)
|
||||
event_log.touch()
|
||||
collector_id.write_text("sha256:collector-stable\n", encoding="utf-8")
|
||||
collector_state.write_text(
|
||||
"true\n" if initial_running else "false\n",
|
||||
encoding="utf-8",
|
||||
@@ -50,37 +48,62 @@ def _run_backup(
|
||||
"""\
|
||||
BACKUP_BASE="${TEST_BACKUP_BASE:?}"
|
||||
RESTIC_PASSWORD_FILE="${TEST_BACKUP_BASE}/password"
|
||||
log_info() { :; }
|
||||
log_warn() { :; }
|
||||
log_error() { :; }
|
||||
log_success() { :; }
|
||||
log_info() { printf 'info %s\\n' "$*" >> "${TEST_EVENT_LOG:?}"; }
|
||||
log_warn() { printf 'warn %s\\n' "$*" >> "${TEST_EVENT_LOG:?}"; }
|
||||
log_error() { printf 'error %s\\n' "$*" >> "${TEST_EVENT_LOG:?}"; }
|
||||
log_success() { printf 'success %s\\n' "$*" >> "${TEST_EVENT_LOG:?}"; }
|
||||
notify_clawbot() { printf 'notify %s\\n' "$*" >> "${TEST_EVENT_LOG:?}"; }
|
||||
build_tags() { :; }
|
||||
cleanup_old_backups() { :; }
|
||||
build_tags() { printf '%s' '--tag signoz'; }
|
||||
cleanup_old_backups() { printf 'retention %s\\n' "$*" >> "${TEST_EVENT_LOG:?}"; }
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
_write_executable(
|
||||
harness / "clickhouse-native-backup.sh",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
printf 'native %s trace=%s run=%s work=%s\\n' \
|
||||
"${1:-}" "${TRACE_ID:-}" "${RUN_ID:-}" "${WORK_ITEM_ID:-}" \
|
||||
>> "${TEST_EVENT_LOG:?}"
|
||||
case "${1:-}" in
|
||||
--check)
|
||||
exit "${FAKE_NATIVE_CHECK_STATUS:-0}"
|
||||
;;
|
||||
--apply)
|
||||
status="${FAKE_NATIVE_APPLY_STATUS:-0}"
|
||||
[ "$status" -eq 0 ] || exit "$status"
|
||||
case "${FAKE_CONTINUITY_MODE:-stable}" in
|
||||
stopped)
|
||||
printf 'false\\n' > "${TEST_COLLECTOR_STATE:?}"
|
||||
sleep 0.08
|
||||
printf 'true\\n' > "${TEST_COLLECTOR_STATE:?}"
|
||||
;;
|
||||
identity)
|
||||
printf 'sha256:collector-replaced\\n' > "${TEST_COLLECTOR_ID:?}"
|
||||
sleep 0.08
|
||||
;;
|
||||
*) sleep 0.08 ;;
|
||||
esac
|
||||
printf 'native-zip\\n' \
|
||||
> "${CLICKHOUSE_NATIVE_OUTPUT_DIR:?}/clickhouse-native-test.zip"
|
||||
printf '{}\\n' \
|
||||
> "${CLICKHOUSE_NATIVE_OUTPUT_DIR:?}/clickhouse-native-test.zip.manifest.json"
|
||||
printf 'db\\ttable\\tengine\\trows\\tbytes\\t%s\\n' \
|
||||
"$(printf 'A%.0s' {1..64})" \
|
||||
> "${CLICKHOUSE_NATIVE_OUTPUT_DIR:?}/clickhouse-native-test.source-inventory.tsv"
|
||||
;;
|
||||
*) exit 64 ;;
|
||||
esac
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
fake_bin / "timeout",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
printf 'timeout %s\n' "$*" >> "${TEST_EVENT_LOG:?}"
|
||||
while [[ "${1:-}" == --kill-after=* ]]; do
|
||||
shift
|
||||
done
|
||||
while [[ "${1:-}" == --kill-after=* ]]; do shift; done
|
||||
shift
|
||||
if [ -n "${FAKE_ARCHIVE_TIMEOUT_TARGET:-}" ] \
|
||||
&& [ "${1:-}" = "docker" ] \
|
||||
&& [[ " $* " == *"${FAKE_ARCHIVE_TIMEOUT_TARGET}"* ]]; then
|
||||
exit 124
|
||||
fi
|
||||
if [ -n "${FAKE_TAR_TIMEOUT_TARGET:-}" ] \
|
||||
&& [ "${1:-}" = "tar" ] \
|
||||
&& [[ " $* " == *"${FAKE_TAR_TIMEOUT_TARGET}"* ]]; then
|
||||
exit 124
|
||||
fi
|
||||
exec "$@"
|
||||
""",
|
||||
)
|
||||
@@ -89,61 +112,12 @@ exec "$@"
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
printf 'docker %s\n' "$*" >> "${TEST_EVENT_LOG:?}"
|
||||
case "${1:-}" in
|
||||
inspect)
|
||||
cat "${TEST_COLLECTOR_STATE:?}"
|
||||
;;
|
||||
stop)
|
||||
if [ "${2:-}" = "signoz-otel-collector" ]; then
|
||||
printf 'false\n' > "${TEST_COLLECTOR_STATE:?}"
|
||||
fi
|
||||
;;
|
||||
start)
|
||||
count=0
|
||||
if [ -e "${TEST_START_COUNT:?}" ]; then
|
||||
count=$(cat "${TEST_START_COUNT}")
|
||||
fi
|
||||
count=$((count + 1))
|
||||
printf '%s\n' "$count" > "${TEST_START_COUNT}"
|
||||
if [ "$count" -le "${FAKE_DOCKER_START_FAILURES:-0}" ]; then
|
||||
exit 1
|
||||
fi
|
||||
printf 'true\n' > "${TEST_COLLECTOR_STATE:?}"
|
||||
;;
|
||||
run)
|
||||
mode=success
|
||||
if [[ " $* " == *"${FAKE_DOCKER_RUN_TARGET:-signoz-clickhouse}"* ]]; then
|
||||
mode="${FAKE_DOCKER_RUN_MODE:-success}"
|
||||
fi
|
||||
if [ "$mode" = "signal" ]; then
|
||||
kill -TERM "${PPID}"
|
||||
sleep 0.1
|
||||
exit 0
|
||||
fi
|
||||
if [ "$mode" = "empty" ]; then
|
||||
exit 0
|
||||
fi
|
||||
if [ "$mode" = "partial_nonzero" ]; then
|
||||
printf 'partial-archive-data\n'
|
||||
exit 23
|
||||
fi
|
||||
printf 'archive-data\n'
|
||||
;;
|
||||
esac
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
fake_bin / "tar",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
printf 'tar %s\n' "$*" >> "${TEST_EVENT_LOG:?}"
|
||||
if [[ " $* " == *"${FAKE_TAR_VERIFY_TARGET:-signoz-clickhouse}"* ]] \
|
||||
&& [ "${FAKE_TAR_VERIFY_MODE:-success}" = "corrupt" ]; then
|
||||
exit 2
|
||||
fi
|
||||
exit 0
|
||||
printf 'docker %s\\n' "$*" >> "${TEST_EVENT_LOG:?}"
|
||||
[ "${1:-}" = "inspect" ] || exit 70
|
||||
printf '%s\\t%s\\t%s\\t%s\\t%s\\n' \
|
||||
"$(tr -d '\\r\\n' < "${TEST_COLLECTOR_ID:?}")" \
|
||||
"$(tr -d '\\r\\n' < "${TEST_COLLECTOR_STATE:?}")" \
|
||||
'2026-07-15T00:00:00Z' '0' 'healthy'
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
@@ -151,18 +125,20 @@ exit 0
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
printf 'restic %s\n' "$*" >> "${TEST_EVENT_LOG:?}"
|
||||
printf 'restic %s\\n' "$*" >> "${TEST_EVENT_LOG:?}"
|
||||
case " $* " in
|
||||
*" backup "*) exit "${FAKE_RESTIC_BACKUP_STATUS:-0}" ;;
|
||||
*" snapshots "*) printf '[{"short_id":"test123"}]\n' ;;
|
||||
*" backup "*)
|
||||
dump_dir="${4:?}"
|
||||
if [ -f "$dump_dir/signoz-metadata-gap.json" ]; then
|
||||
printf 'gap %s\\n' "$(cat "$dump_dir/signoz-metadata-gap.json")" \
|
||||
>> "${TEST_EVENT_LOG:?}"
|
||||
fi
|
||||
exit "${FAKE_RESTIC_BACKUP_STATUS:-0}"
|
||||
;;
|
||||
*" snapshots "*)
|
||||
printf '[{"short_id":"native123"}]\\n'
|
||||
;;
|
||||
esac
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
fake_bin / "du",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
printf '4K\t%s\n' "${2:-unknown}"
|
||||
""",
|
||||
)
|
||||
|
||||
@@ -171,22 +147,20 @@ printf '4K\t%s\n' "${2:-unknown}"
|
||||
"PATH": f"{fake_bin}:{os.environ['PATH']}",
|
||||
"TEST_BACKUP_BASE": str(backup_base),
|
||||
"TEST_EVENT_LOG": str(event_log),
|
||||
"TEST_COLLECTOR_ID": str(collector_id),
|
||||
"TEST_COLLECTOR_STATE": str(collector_state),
|
||||
"TEST_START_COUNT": str(start_count),
|
||||
"FAKE_DOCKER_RUN_MODE": docker_run_mode,
|
||||
"FAKE_DOCKER_RUN_TARGET": docker_run_target,
|
||||
"FAKE_ARCHIVE_TIMEOUT_TARGET": archive_timeout_target,
|
||||
"FAKE_TAR_VERIFY_MODE": tar_verify_mode,
|
||||
"FAKE_TAR_VERIFY_TARGET": tar_verify_target,
|
||||
"FAKE_TAR_TIMEOUT_TARGET": tar_timeout_target,
|
||||
"FAKE_DOCKER_START_FAILURES": str(docker_start_failures),
|
||||
"FAKE_NATIVE_CHECK_STATUS": str(native_check_status),
|
||||
"FAKE_NATIVE_APPLY_STATUS": str(native_apply_status),
|
||||
"FAKE_CONTINUITY_MODE": continuity_mode,
|
||||
"FAKE_RESTIC_BACKUP_STATUS": str(restic_backup_status),
|
||||
"COLLECTOR_RESTORE_MAX_ATTEMPTS": str(restore_max_attempts),
|
||||
"COLLECTOR_RESTORE_RETRY_DELAY_SECONDS": "0",
|
||||
"TRACE_ID": "trace-P0-OBS-002",
|
||||
"RUN_ID": f"run-{tmp_path.name}",
|
||||
"WORK_ITEM_ID": "P0-OBS-002",
|
||||
"SIGNOZ_BACKUP_RECEIPT_ROOT": str(tmp_path / "receipts"),
|
||||
"COLLECTOR_CONTINUITY_POLL_SECONDS": "0.01",
|
||||
"COLLECTOR_DOCKER_TIMEOUT_SECONDS": "1",
|
||||
"ARCHIVE_CREATE_TIMEOUT_SECONDS": "1",
|
||||
"ARCHIVE_VERIFY_TIMEOUT_SECONDS": "1",
|
||||
"TIMEOUT_KILL_AFTER_SECONDS": "1",
|
||||
"BACKUP_SKIP_RETENTION_CLEANUP": "1" if skip_retention else "0",
|
||||
}
|
||||
process = subprocess.Popen(
|
||||
["bash", str(harness / SCRIPT.name)],
|
||||
@@ -210,274 +184,155 @@ def _events(events: list[str], prefix: str) -> list[str]:
|
||||
return [event for event in events if event.startswith(prefix)]
|
||||
|
||||
|
||||
def _run_backup_all_with_signoz_failure(
|
||||
def test_source_sunsets_raw_volumes_and_collector_mutation() -> None:
|
||||
source = SCRIPT.read_text(encoding="utf-8")
|
||||
|
||||
assert "signoz-clickhouse" not in source
|
||||
assert "signoz-sqlite" not in source
|
||||
assert "docker run" not in source
|
||||
assert "docker stop" not in source
|
||||
assert "docker start" not in source
|
||||
assert "create_volume_archive" not in source
|
||||
assert "pending_application_consistent_export" in source
|
||||
assert '"raw_volume_read":false' in source
|
||||
assert "{{.State.StartedAt}}" in source
|
||||
assert "{{.RestartCount}}" in source
|
||||
assert '{{if (index .State "Health")}}' in source
|
||||
assert "--format '{{.Id}}\\t{{.State.Running}}'" not in source
|
||||
assert 'notify_clawbot "success"' not in source
|
||||
assert "no-downtime continuity verifier" in source
|
||||
|
||||
|
||||
def test_success_is_clickhouse_scoped_with_metadata_gap_and_no_downtime(
|
||||
tmp_path: Path,
|
||||
) -> tuple[subprocess.CompletedProcess[str], list[str]]:
|
||||
harness = tmp_path / "backup-all"
|
||||
fake_scripts = tmp_path / "deployed-scripts"
|
||||
event_log = tmp_path / "backup-all-events.log"
|
||||
harness.mkdir()
|
||||
fake_scripts.mkdir()
|
||||
event_log.touch()
|
||||
) -> None:
|
||||
result, events, dump_dir = _run_backup(tmp_path)
|
||||
|
||||
source = BACKUP_ALL.read_text(encoding="utf-8").replace(
|
||||
"/backup/scripts",
|
||||
str(fake_scripts),
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert len(_events(events, "native --check")) == 1
|
||||
assert len(_events(events, "native --apply")) == 1
|
||||
check_identity = _events(events, "native --check")[0].split(" trace=", 1)[1]
|
||||
apply_identity = _events(events, "native --apply")[0].split(" trace=", 1)[1]
|
||||
assert check_identity == apply_identity
|
||||
assert not _events(events, "docker stop")
|
||||
assert not _events(events, "docker start")
|
||||
assert not _events(events, "docker run")
|
||||
assert any("pending_application_consistent_export" in event for event in events)
|
||||
assert len(_events(events, "notify warning signoz ")) == 1
|
||||
assert not _events(events, "notify success ")
|
||||
assert any("SigNoz ClickHouse native 備份完成" in event for event in events)
|
||||
assert any(
|
||||
"--tag run:run-" in event for event in events if event.startswith("restic ")
|
||||
)
|
||||
backup_all = harness / "backup-all.sh"
|
||||
_write_executable(backup_all, source)
|
||||
(harness / "common.sh").write_text(
|
||||
"""\
|
||||
log_info() { :; }
|
||||
log_warn() { :; }
|
||||
log_error() { :; }
|
||||
log_success() { :; }
|
||||
notify_clawbot() { printf 'notify %s\\n' "$*" >> "${TEST_EVENT_LOG:?}"; }
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
for name in (
|
||||
"backup-gitea.sh",
|
||||
"backup-momo.sh",
|
||||
"backup-harbor.sh",
|
||||
"backup-awoooi.sh",
|
||||
"backup-langfuse.sh",
|
||||
"backup-monitoring.sh",
|
||||
"backup-signoz.sh",
|
||||
"backup-open-webui.sh",
|
||||
"backup-clawbot.sh",
|
||||
):
|
||||
status = 17 if name == "backup-signoz.sh" else 0
|
||||
_write_executable(
|
||||
fake_scripts / name,
|
||||
f"#!/bin/bash\nprintf 'run {name}\\n' >> \"${{TEST_EVENT_LOG:?}}\"\nexit {status}\n",
|
||||
)
|
||||
|
||||
result = subprocess.run(
|
||||
["bash", str(backup_all)],
|
||||
env={**os.environ, "TEST_EVENT_LOG": str(event_log)},
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
)
|
||||
return result, event_log.read_text(encoding="utf-8").splitlines()
|
||||
assert any("restic -r" in event and " check " in event for event in events)
|
||||
receipts = list((tmp_path / "receipts").glob("*/restic-snapshot.json"))
|
||||
assert len(receipts) == 1
|
||||
receipt = receipts[0].read_text(encoding="utf-8")
|
||||
assert '"snapshot_id":"native123"' in receipt
|
||||
assert '"offsite_verified":false' in receipt
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_backup_jobs_deploys_the_guarded_signoz_script() -> None:
|
||||
playbook = DEPLOYMENT_PLAYBOOK.read_text(encoding="utf-8")
|
||||
|
||||
assert 'dest: "/backup/scripts/{{ item }}"' in playbook
|
||||
assert "- backup-signoz.sh" in playbook
|
||||
assert "tags: backup_jobs" in playbook
|
||||
|
||||
|
||||
def test_restore_policy_is_bounded_timed_and_independently_verified() -> None:
|
||||
source = SCRIPT.read_text(encoding="utf-8")
|
||||
|
||||
assert 'COLLECTOR_DOCKER_TIMEOUT_SECONDS="${COLLECTOR_DOCKER_TIMEOUT_SECONDS:-10}"' in source
|
||||
assert 'COLLECTOR_RESTORE_MAX_ATTEMPTS="${COLLECTOR_RESTORE_MAX_ATTEMPTS:-3}"' in source
|
||||
assert 'ARCHIVE_CREATE_TIMEOUT_SECONDS="${ARCHIVE_CREATE_TIMEOUT_SECONDS:-1200}"' in source
|
||||
assert 'ARCHIVE_VERIFY_TIMEOUT_SECONDS="${ARCHIVE_VERIFY_TIMEOUT_SECONDS:-1200}"' in source
|
||||
assert 'TIMEOUT_KILL_AFTER_SECONDS="${TIMEOUT_KILL_AFTER_SECONDS:-30}"' in source
|
||||
assert 'timeout --kill-after="${TIMEOUT_KILL_AFTER_SECONDS}"' in source
|
||||
assert 'tar -tzf "${output_file}"' in source
|
||||
assert "docker run" in source
|
||||
assert "|| true" not in source.split("create_volume_archive()", 1)[1].split(
|
||||
"verify_volume_archive()",
|
||||
1,
|
||||
)[0]
|
||||
assert "verify_collector_final_state" in source
|
||||
assert source.index("verify_collector_final_state") < source.rindex(
|
||||
'notify_clawbot "success"'
|
||||
)
|
||||
|
||||
|
||||
def test_canary_retention_skip_is_explicit_and_defaults_off() -> None:
|
||||
source = SCRIPT.read_text(encoding="utf-8")
|
||||
|
||||
assert 'BACKUP_SKIP_RETENTION_CLEANUP="${BACKUP_SKIP_RETENTION_CLEANUP:-0}"' in source
|
||||
assert 'if [ "${BACKUP_SKIP_RETENTION_CLEANUP}" = "1" ]' in source
|
||||
assert source.index('restic -r "${LOCAL_REPO}" backup') < source.index(
|
||||
'if [ "${BACKUP_SKIP_RETENTION_CLEANUP}" = "1" ]'
|
||||
)
|
||||
assert source.index('if [ "${BACKUP_SKIP_RETENTION_CLEANUP}" = "1" ]') < source.index(
|
||||
"if ! verify_collector_final_state"
|
||||
)
|
||||
|
||||
|
||||
def test_collector_is_restored_when_backup_receives_term(tmp_path: Path) -> None:
|
||||
result, events, dump_dir = _run_backup(tmp_path, docker_run_mode="signal")
|
||||
def test_native_check_failure_occurs_before_apply_or_restic(tmp_path: Path) -> None:
|
||||
result, events, dump_dir = _run_backup(tmp_path, native_check_status=19)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert _events(events, "docker stop signoz-otel-collector")
|
||||
assert len(_events(events, "docker start signoz-otel-collector")) == 1
|
||||
assert not _events(events, "notify success ")
|
||||
assert _events(events, "native --check")
|
||||
assert not _events(events, "native --apply")
|
||||
assert not _events(events, "restic ")
|
||||
assert len(_events(events, "notify failed signoz ")) == 1
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_collector_is_restored_when_clickhouse_backup_fails(tmp_path: Path) -> None:
|
||||
result, events, dump_dir = _run_backup(tmp_path, docker_run_mode="empty")
|
||||
def test_native_apply_failure_never_reports_scoped_success(tmp_path: Path) -> None:
|
||||
result, events, dump_dir = _run_backup(tmp_path, native_apply_status=23)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert len(_events(events, "docker start signoz-otel-collector")) == 1
|
||||
assert result.returncode != 0
|
||||
assert _events(events, "native --apply")
|
||||
assert not _events(events, "restic ")
|
||||
assert not _events(events, "notify warning ")
|
||||
assert not _events(events, "notify success ")
|
||||
assert len(_events(events, "notify failed signoz ")) == 1
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_exit_cleanup_does_not_restart_an_already_restored_collector(
|
||||
def test_initially_stopped_collector_fails_without_apply(tmp_path: Path) -> None:
|
||||
result, events, dump_dir = _run_backup(tmp_path, initial_running=False)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert _events(events, "native --check")
|
||||
assert not _events(events, "native --apply")
|
||||
assert not _events(events, "restic ")
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_continuity_observer_rejects_stopped_sample(tmp_path: Path) -> None:
|
||||
result, events, dump_dir = _run_backup(tmp_path, continuity_mode="stopped")
|
||||
|
||||
assert result.returncode != 0
|
||||
assert any("continuity verifier 失敗" in event for event in events)
|
||||
assert not _events(events, "restic ")
|
||||
assert not _events(events, "notify warning ")
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_continuity_observer_rejects_container_identity_drift(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, events, dump_dir = _run_backup(tmp_path, continuity_mode="identity")
|
||||
|
||||
assert result.returncode != 0
|
||||
assert any("continuity verifier 失敗" in event for event in events)
|
||||
assert not _events(events, "restic ")
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_restic_failure_is_nonzero_and_no_completion_notification(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, events, dump_dir = _run_backup(tmp_path, restic_backup_status=17)
|
||||
|
||||
assert result.returncode == 17
|
||||
assert len(_events(events, "docker start signoz-otel-collector")) == 1
|
||||
assert _events(events, "restic ")
|
||||
assert not _events(events, "notify warning ")
|
||||
assert not _events(events, "notify success ")
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_fail_once_restores_within_budget_and_succeeds(tmp_path: Path) -> None:
|
||||
result, events, dump_dir = _run_backup(tmp_path, docker_start_failures=1)
|
||||
def test_canary_retention_skip_is_explicit(tmp_path: Path) -> None:
|
||||
result, events, _dump_dir = _run_backup(tmp_path, skip_retention=True)
|
||||
|
||||
assert result.returncode == 0
|
||||
assert len(_events(events, "docker start signoz-otel-collector")) == 2
|
||||
assert len(_events(events, "notify success ")) == 1
|
||||
assert not _events(events, "notify failed ")
|
||||
assert not dump_dir.exists()
|
||||
assert any("BACKUP_SKIP_RETENTION_CLEANUP=1" in event for event in events)
|
||||
assert not _events(events, "retention ")
|
||||
|
||||
|
||||
def test_permanent_restore_failure_is_nonzero_and_never_notifies_success(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, events, dump_dir = _run_backup(tmp_path, docker_start_failures=99)
|
||||
def test_backup_jobs_deploys_native_adapter_and_orchestrator() -> None:
|
||||
playbook = DEPLOYMENT_PLAYBOOK.read_text(encoding="utf-8")
|
||||
|
||||
assert result.returncode != 0
|
||||
assert len(_events(events, "docker start signoz-otel-collector")) == 3
|
||||
assert len(_events(events, "notify failed ")) == 1
|
||||
assert not _events(events, "notify success ")
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_initially_stopped_fails_closed_without_stop_start_or_success(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, events, dump_dir = _run_backup(tmp_path, initial_running=False)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert not _events(events, "docker stop signoz-otel-collector")
|
||||
assert not _events(events, "docker start signoz-otel-collector")
|
||||
assert len(_events(events, "notify failed ")) == 1
|
||||
assert not _events(events, "notify success ")
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_success_notification_follows_independent_final_running_readback(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, events, _dump_dir = _run_backup(tmp_path)
|
||||
|
||||
assert result.returncode == 0
|
||||
inspect_positions = [
|
||||
index
|
||||
for index, event in enumerate(events)
|
||||
if event.startswith("docker inspect --format")
|
||||
]
|
||||
success_position = next(
|
||||
index
|
||||
for index, event in enumerate(events)
|
||||
if event.startswith("notify success ")
|
||||
assert 'dest: "/backup/scripts/{{ item }}"' in playbook
|
||||
assert "- backup-signoz.sh" in playbook
|
||||
assert "- clickhouse-native-backup.sh" in playbook
|
||||
assert "- clickhouse-native-restore-drill.sh" in playbook
|
||||
assert "- clickhouse-restore-inventory.py" in playbook
|
||||
assert "/backup/config/signoz/clickhouse/config.d/backup_disk.xml" in playbook
|
||||
assert (
|
||||
"/backup/config/signoz/clickhouse/restore-drill/config.d/isolated-cluster.xml"
|
||||
in playbook
|
||||
)
|
||||
assert len(inspect_positions) >= 3
|
||||
assert inspect_positions[-1] < success_position
|
||||
assert "tags: backup_jobs" in playbook
|
||||
|
||||
start_position = next(
|
||||
index
|
||||
for index, event in enumerate(events)
|
||||
if event.startswith("docker start signoz-otel-collector")
|
||||
|
||||
def test_scripts_are_valid_bash() -> None:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"bash",
|
||||
"-n",
|
||||
str(SCRIPT),
|
||||
str(ROOT / "scripts" / "backup" / "clickhouse-native-backup.sh"),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
tar_positions = [
|
||||
index for index, event in enumerate(events) if event.startswith("tar -tzf")
|
||||
]
|
||||
assert len(tar_positions) == 2
|
||||
assert start_position < tar_positions[0]
|
||||
|
||||
|
||||
def test_archive_create_timeout_is_bounded_restores_and_notifies_once(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, events, dump_dir = _run_backup(
|
||||
tmp_path,
|
||||
archive_timeout_target="signoz-clickhouse",
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert len(_events(events, "docker start signoz-otel-collector")) == 1
|
||||
assert len(_events(events, "notify failed ")) == 1
|
||||
assert not _events(events, "notify success ")
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_partial_sqlite_archive_nonzero_fails_overall_and_removes_output(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, events, dump_dir = _run_backup(
|
||||
tmp_path,
|
||||
docker_run_mode="partial_nonzero",
|
||||
docker_run_target="signoz-sqlite",
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert len(_events(events, "docker start signoz-otel-collector")) == 1
|
||||
assert len(_events(events, "notify failed ")) == 1
|
||||
assert not _events(events, "notify success ")
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_corrupt_archive_fails_after_collector_restore_and_notifies_once(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, events, dump_dir = _run_backup(
|
||||
tmp_path,
|
||||
tar_verify_mode="corrupt",
|
||||
tar_verify_target="clickhouse_",
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
start_position = next(
|
||||
index
|
||||
for index, event in enumerate(events)
|
||||
if event.startswith("docker start signoz-otel-collector")
|
||||
)
|
||||
tar_position = next(
|
||||
index for index, event in enumerate(events) if event.startswith("tar -tzf")
|
||||
)
|
||||
assert start_position < tar_position
|
||||
assert len(_events(events, "notify failed ")) == 1
|
||||
assert not _events(events, "notify success ")
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_integrity_verifier_timeout_is_bounded_and_never_reports_success(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, events, dump_dir = _run_backup(
|
||||
tmp_path,
|
||||
tar_timeout_target="clickhouse_",
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert len(_events(events, "docker start signoz-otel-collector")) == 1
|
||||
assert len(_events(events, "notify failed ")) == 1
|
||||
assert not _events(events, "notify success ")
|
||||
assert not dump_dir.exists()
|
||||
|
||||
|
||||
def test_backup_all_propagates_signoz_failure_to_nonzero_terminal(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, events = _run_backup_all_with_signoz_failure(tmp_path)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "run backup-signoz.sh" in events
|
||||
assert any(event.startswith("notify warning all ") for event in events)
|
||||
assert not any(event.startswith("notify success all ") for event in events)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
478
scripts/backup/tests/test_clickhouse_native_backup.py
Normal file
478
scripts/backup/tests/test_clickhouse_native_backup.py
Normal file
@@ -0,0 +1,478 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SCRIPT = ROOT / "scripts" / "backup" / "clickhouse-native-backup.sh"
|
||||
PLAYBOOK = ROOT / "infra" / "ansible" / "playbooks" / "110-devops.yml"
|
||||
|
||||
|
||||
def _write_executable(path: Path, text: str) -> None:
|
||||
path.write_text(text, encoding="utf-8")
|
||||
path.chmod(0o755)
|
||||
|
||||
|
||||
def _install_fakes(fake_bin: Path) -> None:
|
||||
_write_executable(
|
||||
fake_bin / "timeout",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
while [[ "${1:-}" == --kill-after=* ]]; do shift; done
|
||||
shift
|
||||
exec "$@"
|
||||
""",
|
||||
)
|
||||
_write_executable(fake_bin / "flock", "#!/bin/bash\nexit 0\n")
|
||||
_write_executable(
|
||||
fake_bin / "sha256sum",
|
||||
"""\
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import sys
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
payload = sys.stdin.buffer.read()
|
||||
print(f"{hashlib.sha256(payload).hexdigest()} -")
|
||||
else:
|
||||
with open(sys.argv[1], "rb") as source:
|
||||
payload = source.read()
|
||||
print(f"{hashlib.sha256(payload).hexdigest()} {sys.argv[1]}")
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
fake_bin / "unzip",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
printf 'unzip %s\\n' "$*" >> "${TEST_EVENT_LOG:?}"
|
||||
exit "${FAKE_UNZIP_STATUS:-0}"
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
fake_bin / "docker",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
printf 'docker %s\\n' "$*" >> "${TEST_EVENT_LOG:?}"
|
||||
|
||||
find_arg() {
|
||||
local wanted="$1"
|
||||
shift
|
||||
while [ "$#" -gt 0 ]; do
|
||||
if [ "$1" = "$wanted" ]; then
|
||||
printf '%s\\n' "${2:-}"
|
||||
return 0
|
||||
fi
|
||||
shift
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
inspect)
|
||||
printf 'sha256:clickhouse-test-id\\ttrue\\n'
|
||||
;;
|
||||
cp)
|
||||
[ -e "${TEST_SERVER_ARTIFACT:?}" ] || exit 44
|
||||
printf 'clickhouse-native-zip-bytes\\n' > "${3:?}"
|
||||
;;
|
||||
exec)
|
||||
shift
|
||||
container="${1:?}"
|
||||
shift
|
||||
case "${1:-}" in
|
||||
test)
|
||||
shift
|
||||
if [ "${1:-}" = "!" ]; then
|
||||
[ ! -e "${TEST_SERVER_ARTIFACT:?}" ]
|
||||
elif [ "${1:-}" = "-d" ] || [ "${1:-}" = "-w" ]; then
|
||||
[ "${FAKE_DISK_READY:-1}" = "1" ]
|
||||
[ "${2:-}" = "${FAKE_DISK_PATH:-/backups/}" ]
|
||||
else
|
||||
exit 45
|
||||
fi
|
||||
;;
|
||||
rm)
|
||||
rm -f "${TEST_SERVER_ARTIFACT:?}"
|
||||
;;
|
||||
clickhouse-client)
|
||||
query="$(find_arg --query "$@")"
|
||||
operation_id="$(find_arg --param_operation_id "$@" || true)"
|
||||
artifact="$(find_arg --param_artifact "$@" || true)"
|
||||
disk="$(find_arg --param_disk "$@" || true)"
|
||||
case "$query" in
|
||||
'SELECT version()')
|
||||
printf '25.5.1.1\\n'
|
||||
;;
|
||||
*"database='system' AND name='backups'"*)
|
||||
printf '%s\\n' "${FAKE_SYSTEM_BACKUPS_COUNT:-1}"
|
||||
;;
|
||||
'CHECK GRANT BACKUP ON *.*')
|
||||
printf '%s\\n' "${FAKE_BACKUP_GRANT:-1}"
|
||||
;;
|
||||
*"FROM system.disks"*)
|
||||
printf '%s\\n' "${FAKE_DISK_PATH:-/backups/}"
|
||||
;;
|
||||
*"FROM system.databases"*)
|
||||
printf '%s\\n' signoz_analytics signoz_logs signoz_metadata \
|
||||
signoz_meter signoz_metrics signoz_traces
|
||||
if [ "${FAKE_EXTRA_DATABASE:-0}" = "1" ]; then
|
||||
printf 'unexpected_db\\n'
|
||||
fi
|
||||
;;
|
||||
*"FROM system.tables WHERE database IN"*)
|
||||
schema_hash="$(printf 'A%.0s' {1..64})"
|
||||
printf 'signoz_analytics\\tanalytics\\tReplicatedMergeTree\\t10\\t1000\\t%s\\n' "$schema_hash"
|
||||
printf 'signoz_logs\\tlogs_v2\\tReplicatedMergeTree\\t20\\t2000\\t%s\\n' "$schema_hash"
|
||||
printf 'signoz_metadata\\tmetadata\\tMergeTree\\t3\\t300\\t%s\\n' "$schema_hash"
|
||||
printf 'signoz_meter\\tmeter\\tDistributed\\t0\\t0\\t%s\\n' "$schema_hash"
|
||||
printf 'signoz_metrics\\tsamples_v4\\tReplicatedMergeTree\\t40\\t4000\\t%s\\n' "$schema_hash"
|
||||
printf 'signoz_traces\\tsignoz_index_v3\\tReplicatedMergeTree\\t50\\t5000\\t%s\\n' "$schema_hash"
|
||||
;;
|
||||
BACKUP*)
|
||||
case "$query" in
|
||||
*" SETTINGS id='${EXPECTED_FAKE_OPERATION_ID:?}' ASYNC")
|
||||
operation_id="${EXPECTED_FAKE_OPERATION_ID}"
|
||||
;;
|
||||
*) exit 49 ;;
|
||||
esac
|
||||
printf '%s\\n' "$operation_id" > "${TEST_OPERATION_ID:?}"
|
||||
printf '%s\\n' "$artifact" > "${TEST_ARTIFACT_NAME:?}"
|
||||
printf '%s\\n' "$disk" > "${TEST_DISK_NAME:?}"
|
||||
: > "${TEST_SUBMITTED:?}"
|
||||
: > "${TEST_SERVER_ARTIFACT:?}"
|
||||
printf '%s\\tCREATING_BACKUP\\n' "$operation_id"
|
||||
;;
|
||||
*"FROM system.backups WHERE id="*)
|
||||
mode="${FAKE_STATUS_MODE:-success}"
|
||||
if [ "$mode" = "existing_success" ] || [ -e "${TEST_SUBMITTED:?}" ]; then
|
||||
artifact_name="$(cat "${TEST_ARTIFACT_NAME:?}" 2>/dev/null || true)"
|
||||
[ -n "$artifact_name" ] || artifact_name="${EXPECTED_FAKE_ARTIFACT:?}"
|
||||
backup_name="Disk('backups', '$artifact_name')"
|
||||
case "$mode" in
|
||||
failed)
|
||||
printf '%s\\tBACKUP_FAILED\\t0\\t0\\t0\\t0\\t434F44453A20353938\\t%s\\n' \
|
||||
"$backup_name" "$(printf '1%.0s' {1..64})"
|
||||
;;
|
||||
*)
|
||||
: > "${TEST_SERVER_ARTIFACT:?}"
|
||||
printf '%s\\tBACKUP_CREATED\\t12\\t1200\\t2400\\t1100\\tnone\\t%s\\n' \
|
||||
"$backup_name" "$(printf '0%.0s' {1..64})"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
*) exit 46 ;;
|
||||
esac
|
||||
;;
|
||||
*) exit 47 ;;
|
||||
esac
|
||||
;;
|
||||
*) exit 48 ;;
|
||||
esac
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def _harness(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
status_mode: str = "success",
|
||||
extra_database: bool = False,
|
||||
disk_ready: bool = True,
|
||||
unzip_status: int = 0,
|
||||
with_hook: bool = False,
|
||||
) -> tuple[dict[str, str], dict[str, Path]]:
|
||||
fake_bin = tmp_path / "bin"
|
||||
output = tmp_path / "output"
|
||||
receipts = tmp_path / "receipts"
|
||||
fake_bin.mkdir()
|
||||
output.mkdir()
|
||||
receipts.mkdir()
|
||||
_install_fakes(fake_bin)
|
||||
|
||||
event_log = tmp_path / "events.log"
|
||||
event_log.touch()
|
||||
paths = {
|
||||
"output": output,
|
||||
"receipts": receipts,
|
||||
"events": event_log,
|
||||
"submitted": tmp_path / "submitted",
|
||||
"server_artifact": tmp_path / "server-artifact",
|
||||
"operation_id": tmp_path / "operation-id",
|
||||
"artifact_name": tmp_path / "artifact-name",
|
||||
"disk_name": tmp_path / "disk-name",
|
||||
"hook_events": tmp_path / "hook-events.log",
|
||||
}
|
||||
identity_hash = (
|
||||
subprocess.check_output(
|
||||
[str(fake_bin / "sha256sum")],
|
||||
input=b"trace-native\0run-native\0P0-OBS-002",
|
||||
)
|
||||
.decode()
|
||||
.split()[0]
|
||||
)
|
||||
expected_artifact = f"signoz-{identity_hash}.zip"
|
||||
|
||||
env = {
|
||||
**os.environ,
|
||||
"PATH": f"{fake_bin}:{os.environ['PATH']}",
|
||||
"TRACE_ID": "trace-native",
|
||||
"RUN_ID": "run-native",
|
||||
"WORK_ITEM_ID": "P0-OBS-002",
|
||||
"CLICKHOUSE_NATIVE_OUTPUT_DIR": str(output),
|
||||
"CLICKHOUSE_NATIVE_RECEIPT_ROOT": str(receipts),
|
||||
"CLICKHOUSE_NATIVE_COMMAND_TIMEOUT_SECONDS": "2",
|
||||
"CLICKHOUSE_NATIVE_BACKUP_TIMEOUT_SECONDS": "2",
|
||||
"CLICKHOUSE_NATIVE_POLL_INTERVAL_SECONDS": "1",
|
||||
"CLICKHOUSE_NATIVE_KILL_AFTER_SECONDS": "1",
|
||||
"TEST_EVENT_LOG": str(event_log),
|
||||
"TEST_SUBMITTED": str(paths["submitted"]),
|
||||
"TEST_SERVER_ARTIFACT": str(paths["server_artifact"]),
|
||||
"TEST_OPERATION_ID": str(paths["operation_id"]),
|
||||
"TEST_ARTIFACT_NAME": str(paths["artifact_name"]),
|
||||
"TEST_DISK_NAME": str(paths["disk_name"]),
|
||||
"EXPECTED_FAKE_ARTIFACT": expected_artifact,
|
||||
"EXPECTED_FAKE_OPERATION_ID": f"awoooi-{identity_hash}",
|
||||
"FAKE_STATUS_MODE": status_mode,
|
||||
"FAKE_EXTRA_DATABASE": "1" if extra_database else "0",
|
||||
"FAKE_DISK_READY": "1" if disk_ready else "0",
|
||||
"FAKE_UNZIP_STATUS": str(unzip_status),
|
||||
}
|
||||
if with_hook:
|
||||
hook = tmp_path / "restore-verifier-hook.sh"
|
||||
_write_executable(
|
||||
hook,
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
if [ "$1" = "--apply" ]; then
|
||||
test -f "$CLICKHOUSE_NATIVE_ARTIFACT_PATH"
|
||||
test -f "$CLICKHOUSE_NATIVE_SOURCE_INVENTORY_PATH"
|
||||
test -f "$CLICKHOUSE_NATIVE_MANIFEST_PATH"
|
||||
grep -q '"status":"BACKUP_CREATED"' "$CLICKHOUSE_NATIVE_MANIFEST_PATH"
|
||||
fi
|
||||
printf '%s trace=%s run=%s work=%s dbs=%s inventory=%s manifest=%s\\n' \
|
||||
"$1" "$TRACE_ID" "$RUN_ID" "$WORK_ITEM_ID" \
|
||||
"$CLICKHOUSE_NATIVE_EXPECTED_DATABASES" \
|
||||
"$CLICKHOUSE_NATIVE_SOURCE_INVENTORY_PATH" \
|
||||
"${CLICKHOUSE_NATIVE_MANIFEST_PATH:-none}" \
|
||||
>> "${TEST_HOOK_EVENTS:?}"
|
||||
""",
|
||||
)
|
||||
env["CLICKHOUSE_NATIVE_POST_VERIFY_HOOK"] = str(hook)
|
||||
env["TEST_HOOK_EVENTS"] = str(paths["hook_events"])
|
||||
return env, paths
|
||||
|
||||
|
||||
def _run(env: dict[str, str], mode: str) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["bash", str(SCRIPT), mode],
|
||||
env=env,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def _receipts(path: Path) -> list[dict[str, object]]:
|
||||
receipt_file = path / "run-native" / "receipts.jsonl"
|
||||
return [
|
||||
json.loads(line)
|
||||
for line in receipt_file.read_text(encoding="utf-8").splitlines()
|
||||
]
|
||||
|
||||
|
||||
def test_check_validates_exact_scope_without_backup_or_artifact_write(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
env, paths = _harness(tmp_path)
|
||||
result = _run(env, "--check")
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
events = paths["events"].read_text(encoding="utf-8")
|
||||
assert "BACKUP DATABASE" not in events
|
||||
assert "docker cp" not in events
|
||||
assert "docker exec signoz-clickhouse rm" not in events
|
||||
assert not list(paths["output"].iterdir())
|
||||
receipts = _receipts(paths["receipts"])
|
||||
assert receipts[-1]["terminal"] == "check_pass_no_write"
|
||||
|
||||
|
||||
def test_apply_requires_same_run_check_receipt(tmp_path: Path) -> None:
|
||||
env, paths = _harness(tmp_path)
|
||||
result = _run(env, "--apply")
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "same_run_check_pass_receipt_required_before_apply" in result.stderr
|
||||
assert paths["events"].read_text(encoding="utf-8") == ""
|
||||
|
||||
|
||||
def test_apply_submits_exact_six_database_backup_and_verifies_zip(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
env, paths = _harness(tmp_path)
|
||||
assert _run(env, "--check").returncode == 0
|
||||
result = _run(env, "--apply")
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
events = paths["events"].read_text(encoding="utf-8")
|
||||
assert "BACKUP DATABASE signoz_analytics,DATABASE signoz_logs" in events
|
||||
assert "DATABASE signoz_metadata,DATABASE signoz_meter" in events
|
||||
assert "DATABASE signoz_metrics,DATABASE signoz_traces" in events
|
||||
assert "BACKUP ALL" not in events
|
||||
backup_line = next(
|
||||
line for line in events.splitlines() if "--query BACKUP " in line
|
||||
)
|
||||
assert " SETTINGS id='awoooi-" in backup_line
|
||||
assert "id={operation_id:String}" not in backup_line
|
||||
assert "--param_operation_id" not in backup_line
|
||||
assert "docker cp signoz-clickhouse:/backups/signoz-" in events
|
||||
assert "unzip -tq" in events
|
||||
assert "docker exec signoz-clickhouse rm -f -- /backups/signoz-" in events
|
||||
assert not paths["server_artifact"].exists()
|
||||
outputs = list(paths["output"].glob("clickhouse-native-*.zip"))
|
||||
assert len(outputs) == 1
|
||||
manifest = json.loads(Path(str(outputs[0]) + ".manifest.json").read_text())
|
||||
assert manifest["databases"] == [
|
||||
"signoz_analytics",
|
||||
"signoz_logs",
|
||||
"signoz_metadata",
|
||||
"signoz_meter",
|
||||
"signoz_metrics",
|
||||
"signoz_traces",
|
||||
]
|
||||
inventory = list(paths["output"].glob("*.source-inventory.tsv"))
|
||||
assert len(inventory) == 1
|
||||
assert "ReplicatedMergeTree" in inventory[0].read_text(encoding="utf-8")
|
||||
assert "Distributed" in inventory[0].read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_database_allowlist_drift_fails_check(tmp_path: Path) -> None:
|
||||
env, _paths = _harness(tmp_path, extra_database=True)
|
||||
result = _run(env, "--check")
|
||||
assert result.returncode != 0
|
||||
assert "signoz_database_allowlist_drift" in result.stderr
|
||||
|
||||
|
||||
def test_missing_backup_grant_fails_check_before_submission(tmp_path: Path) -> None:
|
||||
env, paths = _harness(tmp_path)
|
||||
env["FAKE_BACKUP_GRANT"] = "0"
|
||||
|
||||
result = _run(env, "--check")
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "native_backup_grant_absent" in result.stderr
|
||||
assert not paths["submitted"].exists()
|
||||
|
||||
|
||||
def test_missing_runtime_backup_disk_fails_check(tmp_path: Path) -> None:
|
||||
env, _paths = _harness(tmp_path, disk_ready=False)
|
||||
result = _run(env, "--check")
|
||||
assert result.returncode != 0
|
||||
assert "backup_disk_directory_absent" in result.stderr
|
||||
|
||||
|
||||
def test_failed_async_terminal_has_exact_cleanup_and_no_local_artifact(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
env, paths = _harness(tmp_path, status_mode="failed")
|
||||
assert _run(env, "--check").returncode == 0
|
||||
result = _run(env, "--apply")
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "native_backup_terminal_BACKUP_FAILED" in result.stderr
|
||||
assert not paths["server_artifact"].exists()
|
||||
assert not list(paths["output"].iterdir())
|
||||
receipt_dir = paths["receipts"] / "run-native"
|
||||
assert list(receipt_dir.glob("*.stderr"))
|
||||
assert list(receipt_dir.glob("*.status"))
|
||||
bounded_errors = list(receipt_dir.glob("*-backup-error-bounded.hex"))
|
||||
assert len(bounded_errors) == 1
|
||||
assert bounded_errors[0].read_text(encoding="utf-8") == ("434F44453A20353938\n")
|
||||
|
||||
|
||||
def test_zip_failure_removes_local_partial_and_preserves_server_evidence(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
env, paths = _harness(tmp_path, unzip_status=7)
|
||||
assert _run(env, "--check").returncode == 0
|
||||
result = _run(env, "--apply")
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "native_backup_archive_integrity_failed" in result.stderr
|
||||
assert paths["server_artifact"].exists()
|
||||
assert not list(paths["output"].iterdir())
|
||||
receipts = _receipts(paths["receipts"])
|
||||
assert any(
|
||||
item["stage"] == "cleanup" and item["terminal"] == "preserved"
|
||||
for item in receipts
|
||||
)
|
||||
|
||||
|
||||
def test_restore_verifier_hook_uses_check_and_apply_contract(tmp_path: Path) -> None:
|
||||
env, paths = _harness(tmp_path, with_hook=True)
|
||||
assert _run(env, "--check").returncode == 0
|
||||
result = _run(env, "--apply")
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
hook_events = paths["hook_events"].read_text(encoding="utf-8").splitlines()
|
||||
assert sum(event.startswith("--check ") for event in hook_events) == 1
|
||||
assert sum(event.startswith("--apply ") for event in hook_events) == 1
|
||||
assert all("signoz_analytics,signoz_logs" in event for event in hook_events)
|
||||
assert any(
|
||||
"manifest=" in event and "manifest=none" not in event
|
||||
for event in hook_events
|
||||
if event.startswith("--apply ")
|
||||
)
|
||||
|
||||
|
||||
def test_verified_local_artifact_is_idempotently_reused(tmp_path: Path) -> None:
|
||||
env, paths = _harness(tmp_path)
|
||||
assert _run(env, "--check").returncode == 0
|
||||
assert _run(env, "--apply").returncode == 0
|
||||
submissions_before = (
|
||||
paths["events"].read_text(encoding="utf-8").count("BACKUP DATABASE")
|
||||
)
|
||||
|
||||
result = _run(env, "--apply")
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
submissions_after = (
|
||||
paths["events"].read_text(encoding="utf-8").count("BACKUP DATABASE")
|
||||
)
|
||||
assert submissions_after == submissions_before
|
||||
receipts = _receipts(paths["receipts"])
|
||||
assert any(
|
||||
item["stage"] == "idempotency" and item["terminal"] == "reused"
|
||||
for item in receipts
|
||||
)
|
||||
|
||||
|
||||
def test_source_contract_has_no_raw_volume_fallback_and_is_deployed() -> None:
|
||||
source = SCRIPT.read_text(encoding="utf-8")
|
||||
playbook = PLAYBOOK.read_text(encoding="utf-8")
|
||||
|
||||
assert "BACKUP ${BACKUP_SCOPE_SQL}" in source
|
||||
assert "SETTINGS id='${OPERATION_ID}' ASYNC" in source
|
||||
assert "SETTINGS id={operation_id:String}" not in source
|
||||
assert "BACKUP ALL" not in source
|
||||
assert ".zip" in source
|
||||
assert "unzip -tq" in source
|
||||
assert "--format $'{{.Id}}\\t{{.State.Running}}'" in source
|
||||
assert "--format '{{.Id}}\\t{{.State.Running}}'" not in source
|
||||
assert "docker run" not in source
|
||||
assert "clickhouse-native-backup.sh" in playbook
|
||||
result = subprocess.run(
|
||||
["bash", "-n", str(SCRIPT)],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
848
scripts/backup/tests/test_clickhouse_native_restore_drill.py
Normal file
848
scripts/backup/tests/test_clickhouse_native_restore_drill.py
Normal file
@@ -0,0 +1,848 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "clickhouse-native-restore-drill.sh"
|
||||
INVENTORY_HELPER = ROOT / "clickhouse-restore-inventory.py"
|
||||
REPO_ROOT = ROOT.parents[1]
|
||||
CLICKHOUSE_IMAGE_ID = (
|
||||
"sha256:8248e5926d7304400e44ecdf0c7181fbde28e6a9b1d647780eca839f34c00cc6"
|
||||
)
|
||||
ZOOKEEPER_IMAGE_ID = (
|
||||
"sha256:3ab0e8f032ab58f14ac1e929ac40305a4a464cf320bdfe4151d62d6922729ba0"
|
||||
)
|
||||
EMPTY_SHA256 = "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"
|
||||
EXPECTED_DATABASES = [
|
||||
"signoz_analytics",
|
||||
"signoz_logs",
|
||||
"signoz_metadata",
|
||||
"signoz_meter",
|
||||
"signoz_metrics",
|
||||
"signoz_traces",
|
||||
]
|
||||
|
||||
|
||||
def _inventory(
|
||||
*,
|
||||
restored_delta: int = 0,
|
||||
engine_mismatch: bool = False,
|
||||
schema_mismatch: bool = False,
|
||||
) -> str:
|
||||
rows = [
|
||||
("signoz_analytics", "events", "ReplicatedMergeTree", 10, 100),
|
||||
("signoz_analytics", "events_all", "Distributed", 10, 100),
|
||||
("signoz_logs", "logs_v2", "ReplicatedMergeTree", 20, 200),
|
||||
("signoz_metadata", "resource_attrs", "ReplicatedMergeTree", 2, 20),
|
||||
("signoz_meter", "meter", "ReplicatedMergeTree", 3, 30),
|
||||
("signoz_metrics", "samples_v4", "ReplicatedMergeTree", 30, 300),
|
||||
("signoz_metrics", "time_series_v4", "ReplicatedMergeTree", 12, 120),
|
||||
("signoz_traces", "signoz_index_v3", "ReplicatedMergeTree", 40, 400),
|
||||
]
|
||||
rendered: list[str] = []
|
||||
for index, (database, table, engine, row_count, byte_count) in enumerate(rows):
|
||||
if engine_mismatch and table == "meter":
|
||||
engine = "MergeTree"
|
||||
schema_hash = (
|
||||
hashlib.sha256(f"{database}.{table}:{engine}".encode("utf-8"))
|
||||
.hexdigest()
|
||||
.upper()
|
||||
)
|
||||
if schema_mismatch and table == "meter":
|
||||
schema_hash = "F" * 64
|
||||
rendered.append(
|
||||
"\t".join(
|
||||
(
|
||||
database,
|
||||
table,
|
||||
engine,
|
||||
str(row_count + (restored_delta if index == 0 else 0)),
|
||||
str(byte_count + (restored_delta if index == 0 else 0)),
|
||||
schema_hash,
|
||||
)
|
||||
)
|
||||
)
|
||||
return "\n".join(rendered) + "\n"
|
||||
|
||||
|
||||
def _write_backup_fixture(tmp_path: Path) -> tuple[Path, Path, Path]:
|
||||
artifact = tmp_path / "clickhouse-native.zip.partial"
|
||||
with zipfile.ZipFile(artifact, "w") as archive:
|
||||
archive.writestr(".backup", "bounded native backup fixture\n")
|
||||
inventory = tmp_path / "source-inventory.tsv.partial"
|
||||
inventory.write_text(_inventory(), encoding="utf-8")
|
||||
artifact_sha = hashlib.sha256(artifact.read_bytes()).hexdigest()
|
||||
inventory_sha = hashlib.sha256(inventory.read_bytes()).hexdigest()
|
||||
manifest = tmp_path / "manifest.json.partial"
|
||||
manifest.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"trace_id": "backup-trace",
|
||||
"run_id": "backup-run",
|
||||
"work_item_id": "P0-OBS-002",
|
||||
"operation_id": "awoooi-backup-operation",
|
||||
"databases": EXPECTED_DATABASES,
|
||||
"source_inventory_sha256": inventory_sha,
|
||||
"sha256": artifact_sha,
|
||||
"size_bytes": artifact.stat().st_size,
|
||||
"status": "BACKUP_CREATED",
|
||||
},
|
||||
separators=(",", ":"),
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return artifact, manifest, inventory
|
||||
|
||||
|
||||
def _write_fake_docker(tmp_path: Path) -> tuple[Path, Path, Path]:
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
state_dir = tmp_path / "docker-state"
|
||||
state_dir.mkdir()
|
||||
log_path = tmp_path / "docker.log"
|
||||
docker = bin_dir / "docker"
|
||||
docker.write_text(
|
||||
r"""#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf '%s\n' "$*" >> "${FAKE_DOCKER_LOG:?}"
|
||||
state="${FAKE_DOCKER_STATE:?}"
|
||||
cmd="${1:?}"
|
||||
shift
|
||||
last_arg() { eval "printf '%s' \"\${$#}\""; }
|
||||
|
||||
case "$cmd" in
|
||||
info)
|
||||
[ "${FAKE_DOCKER_INFO_FAIL:-0}" = 0 ] || exit 78
|
||||
printf '25.0.0\\n'
|
||||
;;
|
||||
image)
|
||||
sub="${1:?}"
|
||||
shift
|
||||
[ "$sub" = inspect ] || exit 90
|
||||
image="$(last_arg "$@")"
|
||||
case "$image" in
|
||||
clickhouse/clickhouse-server:25.5.6) printf '%s\n' "${FAKE_CLICKHOUSE_IMAGE_ID:?}" ;;
|
||||
signoz/zookeeper:3.7.1) printf '%s\n' "${FAKE_ZOOKEEPER_IMAGE_ID:?}" ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
;;
|
||||
container)
|
||||
sub="${1:?}"
|
||||
shift
|
||||
case "$sub" in
|
||||
inspect)
|
||||
format=""
|
||||
if [ "${1:-}" = --format ]; then
|
||||
format="$2"
|
||||
shift 2
|
||||
fi
|
||||
name="${1:?}"
|
||||
[ -f "$state/container-$name" ] || exit 1
|
||||
if [[ "$format" == *Labels* ]]; then
|
||||
if [ "${FAKE_LABEL_INSPECT_HANG:-0}" = 1 ] && [[ "$name" == *ch-restore* ]]; then
|
||||
sleep 10
|
||||
fi
|
||||
cat "$state/container-label-$name"
|
||||
fi
|
||||
;;
|
||||
ls)
|
||||
for item in "$state"/container-*; do
|
||||
[ -e "$item" ] || continue
|
||||
case "$item" in
|
||||
*-label-*|*-network-*|*-image-*) continue ;;
|
||||
esac
|
||||
basename "$item" | sed 's/^container-//'
|
||||
done
|
||||
;;
|
||||
*) exit 90 ;;
|
||||
esac
|
||||
;;
|
||||
inspect)
|
||||
format=""
|
||||
if [ "${1:-}" = --format ]; then
|
||||
format="$2"
|
||||
shift 2
|
||||
fi
|
||||
name="${1:?}"
|
||||
[ -f "$state/container-$name" ] || exit 1
|
||||
case "$format" in
|
||||
*State.Running*) printf 'true\n' ;;
|
||||
*HostConfig.NetworkMode*) cat "$state/container-network-$name" ;;
|
||||
*.Image*) cat "$state/container-image-$name" ;;
|
||||
*) printf '%s\n' "fake-$name" ;;
|
||||
esac
|
||||
;;
|
||||
network)
|
||||
sub="${1:?}"
|
||||
shift
|
||||
case "$sub" in
|
||||
create)
|
||||
name="$(last_arg "$@")"
|
||||
label=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
if [ "$1" = --label ]; then label="${2#*=}"; shift 2; else shift; fi
|
||||
done
|
||||
touch "$state/network-$name"
|
||||
printf '%s\n' "$label" > "$state/network-label-$name"
|
||||
printf '%s\n' "$name" > "$state/network-name"
|
||||
printf '%s\n' "$name"
|
||||
;;
|
||||
inspect)
|
||||
format=""
|
||||
if [ "${1:-}" = --format ]; then
|
||||
format="$2"
|
||||
shift 2
|
||||
fi
|
||||
name="${1:?}"
|
||||
[ -f "$state/network-$name" ] || exit 1
|
||||
if [[ "$format" == *Internal* ]]; then printf 'true\n'; fi
|
||||
if [[ "$format" == *Labels* ]]; then cat "$state/network-label-$name"; fi
|
||||
;;
|
||||
ls)
|
||||
for item in "$state"/network-*; do
|
||||
[ -e "$item" ] || continue
|
||||
case "$item" in *-label-*|*/network-name) continue ;; esac
|
||||
basename "$item" | sed 's/^network-//'
|
||||
done
|
||||
;;
|
||||
rm)
|
||||
name="${1:?}"
|
||||
rm -f "$state/network-$name" "$state/network-label-$name" "$state/network-name"
|
||||
printf '%s\n' "$name"
|
||||
;;
|
||||
*) exit 90 ;;
|
||||
esac
|
||||
;;
|
||||
volume)
|
||||
sub="${1:?}"
|
||||
shift
|
||||
case "$sub" in
|
||||
create)
|
||||
name="$(last_arg "$@")"
|
||||
label=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
if [ "$1" = --label ]; then label="${2#*=}"; shift 2; else shift; fi
|
||||
done
|
||||
touch "$state/volume-$name"
|
||||
printf '%s\n' "$label" > "$state/volume-label-$name"
|
||||
printf '%s\n' "$name"
|
||||
;;
|
||||
inspect)
|
||||
format=""
|
||||
if [ "${1:-}" = --format ]; then format="$2"; shift 2; fi
|
||||
name="${1:?}"
|
||||
[ -f "$state/volume-$name" ] || exit 1
|
||||
if [[ "$format" == *Labels* ]]; then cat "$state/volume-label-$name"; fi
|
||||
;;
|
||||
ls)
|
||||
for item in "$state"/volume-*; do
|
||||
[ -e "$item" ] || continue
|
||||
case "$item" in *-label-*) continue ;; esac
|
||||
basename "$item" | sed 's/^volume-//'
|
||||
done
|
||||
;;
|
||||
rm)
|
||||
name="${1:?}"
|
||||
rm -f "$state/volume-$name" "$state/volume-label-$name"
|
||||
if [[ "$name" == *-backup ]]; then
|
||||
rm -f "$state/staged-artifact-sha256"
|
||||
fi
|
||||
printf '%s\n' "$name"
|
||||
;;
|
||||
*) exit 90 ;;
|
||||
esac
|
||||
;;
|
||||
run)
|
||||
name=""
|
||||
replica=""
|
||||
label=""
|
||||
network=""
|
||||
image=""
|
||||
expected_artifact_sha256=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--name)
|
||||
name="$2"
|
||||
shift 2
|
||||
;;
|
||||
--env)
|
||||
if [[ "$2" == CLICKHOUSE_RESTORE_REPLICA=* ]]; then
|
||||
replica="${2#*=}"
|
||||
fi
|
||||
if [[ "$2" == EXPECTED_ARTIFACT_SHA256=* ]]; then
|
||||
expected_artifact_sha256="${2#*=}"
|
||||
fi
|
||||
shift 2
|
||||
;;
|
||||
--label)
|
||||
label="${2#*=}"
|
||||
shift 2
|
||||
;;
|
||||
--network)
|
||||
network="$2"
|
||||
shift 2
|
||||
;;
|
||||
--network-alias|--restart|--mount|--user|--entrypoint)
|
||||
shift 2
|
||||
;;
|
||||
--detach|--pull=never)
|
||||
shift
|
||||
;;
|
||||
clickhouse/clickhouse-server:25.5.6|signoz/zookeeper:3.7.1)
|
||||
image="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[ -n "$name" ] || exit 91
|
||||
touch "$state/container-$name"
|
||||
printf '%s\n' "$label" > "$state/container-label-$name"
|
||||
printf '%s\n' "$network" > "$state/container-network-$name"
|
||||
case "$image" in
|
||||
clickhouse/clickhouse-server:25.5.6)
|
||||
printf '%s\n' "${FAKE_CLICKHOUSE_IMAGE_ID:?}" > "$state/container-image-$name"
|
||||
;;
|
||||
signoz/zookeeper:3.7.1)
|
||||
printf '%s\n' "${FAKE_ZOOKEEPER_IMAGE_ID:?}" > "$state/container-image-$name"
|
||||
;;
|
||||
*) exit 96 ;;
|
||||
esac
|
||||
if [ -n "$replica" ]; then printf '%s\n' "$replica" > "$state/replica"; fi
|
||||
if [ "$network" = none ]; then
|
||||
if [ "${FAKE_STAGING_HANG:-0}" = 1 ]; then sleep 10; fi
|
||||
[ "${FAKE_STAGING_FAIL:-0}" = 0 ] || exit 94
|
||||
staged_hash="$expected_artifact_sha256"
|
||||
if [ "${FAKE_STAGING_HASH_MISMATCH:-0}" = 1 ]; then
|
||||
staged_hash="$(printf '0%.0s' {1..64})"
|
||||
fi
|
||||
printf '%s\n' "$staged_hash" > "$state/staged-artifact-sha256"
|
||||
printf '%s\n' "$staged_hash"
|
||||
else
|
||||
printf 'fake-container-%s\n' "$name"
|
||||
fi
|
||||
;;
|
||||
exec)
|
||||
container="${1:?}"
|
||||
shift
|
||||
[ -f "$state/container-$container" ] || exit 1
|
||||
if [ "${1:-}" = sha256sum ]; then
|
||||
[ -f "$state/staged-artifact-sha256" ] || exit 97
|
||||
artifact_hash="$(cat "$state/staged-artifact-sha256")"
|
||||
if [ "${FAKE_CLICKHOUSE_HASH_MISMATCH:-0}" = 1 ]; then
|
||||
artifact_hash="$(printf 'f%.0s' {1..64})"
|
||||
fi
|
||||
printf '%s /backups/native-backup.zip\n' "$artifact_hash"
|
||||
exit 0
|
||||
fi
|
||||
query=""
|
||||
operation=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--query)
|
||||
query="$2"
|
||||
shift 2
|
||||
;;
|
||||
--param_operation_id)
|
||||
operation="$2"
|
||||
shift 2
|
||||
;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
case "$query" in
|
||||
CHECK\ TABLE*)
|
||||
if [ "${FAKE_CHECK_TABLE_FAIL:-0}" = 1 ]; then printf '0\n'; else printf '1\n'; fi
|
||||
;;
|
||||
RESTORE\ *)
|
||||
case "$query" in
|
||||
*" SETTINGS id='${EXPECTED_FAKE_RESTORE_OPERATION_ID:?}' ASYNC")
|
||||
operation="${EXPECTED_FAKE_RESTORE_OPERATION_ID}"
|
||||
;;
|
||||
*) exit 93 ;;
|
||||
esac
|
||||
touch "$state/restored"
|
||||
printf '%s\tRESTORING\n' "$operation"
|
||||
;;
|
||||
*FROM\ system.backups*)
|
||||
printf "Disk('backups', 'native-backup.zip')\tRESTORED\t100\t1000\t%s\n" "${EMPTY_SHA256:?}"
|
||||
;;
|
||||
*FROM\ system.disks*) printf '/backups/\n' ;;
|
||||
*FROM\ system.macros*)
|
||||
printf 'replica\t%s\nshard\t01\n' "$(cat "$state/replica")"
|
||||
;;
|
||||
*FROM\ system.clusters*) printf 'cluster\trestore-clickhouse\t9000\n' ;;
|
||||
*FROM\ system.zookeeper*) printf '1\n' ;;
|
||||
*"SELECT count() FROM system.databases"*) printf '0\n' ;;
|
||||
*"SELECT count() FROM system.tables"*) printf '0\n' ;;
|
||||
*"SELECT database,name,engine"*) cat "${FAKE_RESTORED_INVENTORY:?}" ;;
|
||||
"SELECT 1") printf '1\n' ;;
|
||||
*) printf 'UNHANDLED QUERY: %s\n' "$query" >&2; exit 92 ;;
|
||||
esac
|
||||
;;
|
||||
logs)
|
||||
printf 'bounded fake container log\n'
|
||||
;;
|
||||
rm)
|
||||
if [ "${1:-}" = -f ]; then shift; fi
|
||||
name="${1:?}"
|
||||
rm -f "$state/container-$name" "$state/container-label-$name" \
|
||||
"$state/container-network-$name" "$state/container-image-$name"
|
||||
printf '%s\n' "$name"
|
||||
;;
|
||||
*) exit 99 ;;
|
||||
esac
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
docker.chmod(0o755)
|
||||
return bin_dir, state_dir, log_path
|
||||
|
||||
|
||||
def _environment(
|
||||
tmp_path: Path,
|
||||
artifact: Path,
|
||||
manifest: Path,
|
||||
inventory: Path,
|
||||
restored_inventory: Path,
|
||||
) -> dict[str, str]:
|
||||
bin_dir, state_dir, log_path = _write_fake_docker(tmp_path)
|
||||
return os.environ | {
|
||||
"PATH": f"{bin_dir}:{os.environ['PATH']}",
|
||||
"TRACE_ID": "trace-restore-test",
|
||||
"RUN_ID": "run-restore-test",
|
||||
"WORK_ITEM_ID": "P0-OBS-002",
|
||||
"CLICKHOUSE_NATIVE_ARTIFACT_PATH": str(artifact),
|
||||
"CLICKHOUSE_NATIVE_MANIFEST_PATH": str(manifest),
|
||||
"CLICKHOUSE_NATIVE_SOURCE_INVENTORY_PATH": str(inventory),
|
||||
"CLICKHOUSE_NATIVE_ARTIFACT_SHA256": hashlib.sha256(
|
||||
artifact.read_bytes()
|
||||
).hexdigest(),
|
||||
"CLICKHOUSE_NATIVE_OPERATION_ID": "awoooi-backup-operation",
|
||||
"CLICKHOUSE_NATIVE_EXPECTED_DATABASES": ",".join(EXPECTED_DATABASES),
|
||||
"CLICKHOUSE_RESTORE_RECEIPT_ROOT": str(tmp_path / "receipts"),
|
||||
"CLICKHOUSE_RESTORE_POLL_INTERVAL_SECONDS": "1",
|
||||
"CLICKHOUSE_RESTORE_STARTUP_TIMEOUT_SECONDS": "5",
|
||||
"CLICKHOUSE_RESTORE_TIMEOUT_SECONDS": "5",
|
||||
"CLICKHOUSE_RESTORE_COMMAND_TIMEOUT_SECONDS": "5",
|
||||
"FAKE_DOCKER_LOG": str(log_path),
|
||||
"FAKE_DOCKER_STATE": str(state_dir),
|
||||
"FAKE_CLICKHOUSE_IMAGE_ID": CLICKHOUSE_IMAGE_ID,
|
||||
"FAKE_ZOOKEEPER_IMAGE_ID": ZOOKEEPER_IMAGE_ID,
|
||||
"FAKE_RESTORED_INVENTORY": str(restored_inventory),
|
||||
"EXPECTED_FAKE_RESTORE_OPERATION_ID": "restore-"
|
||||
+ hashlib.sha256(
|
||||
b"trace-restore-test\0run-restore-test\0P0-OBS-002"
|
||||
).hexdigest(),
|
||||
"EMPTY_SHA256": EMPTY_SHA256,
|
||||
}
|
||||
|
||||
|
||||
def _run(mode: str, env: dict[str, str]) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
["bash", str(SCRIPT), mode],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
|
||||
def _status(tmp_path: Path) -> dict[str, object]:
|
||||
return json.loads(
|
||||
(tmp_path / "receipts" / "run-restore-test" / "status.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _assert_ephemeral_resources_absent(state_dir: Path) -> None:
|
||||
assert not list(state_dir.glob("container-*"))
|
||||
assert not list(state_dir.glob("volume-*"))
|
||||
assert not list(state_dir.glob("network-*"))
|
||||
assert not (state_dir / "staged-artifact-sha256").exists()
|
||||
|
||||
|
||||
def test_hook_check_uses_env_contract_without_runtime_creation(tmp_path: Path) -> None:
|
||||
artifact, manifest, inventory = _write_backup_fixture(tmp_path)
|
||||
restored = tmp_path / "restored.tsv"
|
||||
restored.write_text(_inventory(restored_delta=3), encoding="utf-8")
|
||||
env = _environment(tmp_path, artifact, manifest, inventory, restored)
|
||||
|
||||
result = _run("--check", env)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "terminal=check_pass_no_runtime_write" in result.stdout
|
||||
docker_log = Path(env["FAKE_DOCKER_LOG"]).read_text(encoding="utf-8")
|
||||
assert "image inspect" in docker_log
|
||||
assert "network create" not in docker_log
|
||||
assert "volume create" not in docker_log
|
||||
assert not any(line.startswith("run ") for line in docker_log.splitlines())
|
||||
status = _status(tmp_path)
|
||||
assert status["check_pass"] is True
|
||||
assert status["ephemeral_runtime_created"] is False
|
||||
assert status["production_network_attached"] is False
|
||||
assert status["production_restore_performed"] is False
|
||||
|
||||
|
||||
def test_apply_restores_on_internal_pair_and_cleans_every_resource(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
artifact, manifest, inventory = _write_backup_fixture(tmp_path)
|
||||
restored = tmp_path / "restored.tsv"
|
||||
restored.write_text(_inventory(restored_delta=3), encoding="utf-8")
|
||||
env = _environment(tmp_path, artifact, manifest, inventory, restored)
|
||||
check = _run("--check", env)
|
||||
assert check.returncode == 0, check.stderr
|
||||
|
||||
result = _run("--apply", env)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "terminal=verified" in result.stdout
|
||||
docker_lines = Path(env["FAKE_DOCKER_LOG"]).read_text(encoding="utf-8").splitlines()
|
||||
network_create = next(
|
||||
line for line in docker_lines if line.startswith("network create")
|
||||
)
|
||||
assert "--internal" in network_create
|
||||
run_lines = [line for line in docker_lines if line.startswith("run ")]
|
||||
assert len(run_lines) == 3
|
||||
assert all("--pull=never" in line for line in run_lines)
|
||||
assert all("--publish" not in line and " -p " not in line for line in run_lines)
|
||||
|
||||
staging_run = next(line for line in run_lines if "--network none" in line)
|
||||
zookeeper_run = next(line for line in run_lines if "signoz/zookeeper:3.7.1" in line)
|
||||
clickhouse_run = next(
|
||||
line for line in run_lines if "--network-alias restore-clickhouse" in line
|
||||
)
|
||||
assert "--user 0:0" in staging_run
|
||||
assert "--entrypoint /bin/sh" in staging_run
|
||||
assert (
|
||||
f"type=bind,src={artifact},dst=/tmp/awoooi-source-native-backup.zip,readonly"
|
||||
in staging_run
|
||||
)
|
||||
assert sum(f"src={artifact}" in line for line in run_lines) == 1
|
||||
assert "type=volume,src=awoooi-ch-restore-" in staging_run
|
||||
assert "-backup,dst=/backups" in staging_run
|
||||
assert "ALLOW_ANONYMOUS_LOGIN=yes" in zookeeper_run
|
||||
assert "--network awoooi-ch-restore-" in zookeeper_run
|
||||
assert "--network awoooi-ch-restore-" in clickhouse_run
|
||||
assert str(artifact) not in clickhouse_run
|
||||
assert "type=volume,src=awoooi-ch-restore-" in clickhouse_run
|
||||
assert "-backup,dst=/backups" in clickhouse_run
|
||||
assert "dst=/backups,readonly" not in clickhouse_run
|
||||
assert (
|
||||
"dst=/etc/clickhouse-server/config.d/awoooi-backup-disk.xml,readonly"
|
||||
in clickhouse_run
|
||||
)
|
||||
assert (
|
||||
"dst=/etc/clickhouse-server/config.d/awoooi-restore-isolation.xml,readonly"
|
||||
in clickhouse_run
|
||||
)
|
||||
hash_readback = next(
|
||||
line
|
||||
for line in docker_lines
|
||||
if line.startswith("exec ") and "sha256sum /backups/native-backup.zip" in line
|
||||
)
|
||||
assert "exec awoooi-ch-restore-" in hash_readback
|
||||
restore_line = next(line for line in docker_lines if "RESTORE DATABASE" in line)
|
||||
assert "allow_non_empty_tables" not in restore_line
|
||||
assert " SETTINGS id='restore-" in restore_line
|
||||
assert "id={operation_id:String}" not in restore_line
|
||||
macro_line = next(line for line in docker_lines if "FROM system.macros" in line)
|
||||
assert "SELECT macro,substitution" in macro_line
|
||||
assert "SELECT name,value" not in macro_line
|
||||
state_dir = Path(env["FAKE_DOCKER_STATE"])
|
||||
_assert_ephemeral_resources_absent(state_dir)
|
||||
status = _status(tmp_path)
|
||||
assert status["terminal"] == "verified"
|
||||
assert status["check_pass"] is True
|
||||
assert status["restore_terminal"] == "RESTORED"
|
||||
assert status["manifest_parity"] == 1
|
||||
assert status["check_table_count"] == status["check_table_pass_count"] == 7
|
||||
assert status["critical_nonzero_count"] == 4
|
||||
assert status["row_delta"] == 3
|
||||
expected_hash = hashlib.sha256(artifact.read_bytes()).hexdigest()
|
||||
assert status["artifact_sha256"] == expected_hash
|
||||
assert status["staged_artifact_sha256"] == expected_hash
|
||||
assert status["clickhouse_artifact_sha256"] == expected_hash
|
||||
assert status["artifact_staging_verified"] is True
|
||||
assert status["clickhouse_artifact_readback_verified"] is True
|
||||
assert status["staging_network_mode"] == "none"
|
||||
assert status["artifact_source_mount"] == "read_only_staging_only"
|
||||
assert status["backup_volume_mount"] == "writable_ephemeral"
|
||||
assert status["cleanup_verified"] is True
|
||||
|
||||
|
||||
def test_apply_failure_still_cleans_pair_network_and_volumes(tmp_path: Path) -> None:
|
||||
artifact, manifest, inventory = _write_backup_fixture(tmp_path)
|
||||
restored = tmp_path / "restored.tsv"
|
||||
restored.write_text(_inventory(), encoding="utf-8")
|
||||
env = _environment(tmp_path, artifact, manifest, inventory, restored)
|
||||
assert _run("--check", env).returncode == 0
|
||||
env["FAKE_CHECK_TABLE_FAIL"] = "1"
|
||||
|
||||
result = _run("--apply", env)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "terminal=failed" in result.stdout
|
||||
state_dir = Path(env["FAKE_DOCKER_STATE"])
|
||||
_assert_ephemeral_resources_absent(state_dir)
|
||||
status = _status(tmp_path)
|
||||
assert status["terminal"] == "failed"
|
||||
assert status["cleanup_verified"] is True
|
||||
assert status["apply_pass"] is False
|
||||
|
||||
|
||||
def test_artifact_staging_failure_cleans_exact_resources(tmp_path: Path) -> None:
|
||||
artifact, manifest, inventory = _write_backup_fixture(tmp_path)
|
||||
restored = tmp_path / "restored.tsv"
|
||||
restored.write_text(_inventory(), encoding="utf-8")
|
||||
env = _environment(tmp_path, artifact, manifest, inventory, restored)
|
||||
assert _run("--check", env).returncode == 0
|
||||
env["FAKE_STAGING_FAIL"] = "1"
|
||||
|
||||
result = _run("--apply", env)
|
||||
|
||||
assert result.returncode == 1
|
||||
_assert_ephemeral_resources_absent(Path(env["FAKE_DOCKER_STATE"]))
|
||||
status = _status(tmp_path)
|
||||
assert status["terminal"] == "failed"
|
||||
assert status["reason"] == "isolated_artifact_staging_failed"
|
||||
assert status["artifact_staging_verified"] is False
|
||||
assert status["cleanup_verified"] is True
|
||||
|
||||
|
||||
def test_artifact_staging_hang_is_bounded_and_cleans_exact_resources(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
artifact, manifest, inventory = _write_backup_fixture(tmp_path)
|
||||
restored = tmp_path / "restored.tsv"
|
||||
restored.write_text(_inventory(), encoding="utf-8")
|
||||
env = _environment(tmp_path, artifact, manifest, inventory, restored)
|
||||
env["CLICKHOUSE_RESTORE_COMMAND_TIMEOUT_SECONDS"] = "1"
|
||||
assert _run("--check", env).returncode == 0
|
||||
env["FAKE_STAGING_HANG"] = "1"
|
||||
|
||||
result = subprocess.run(
|
||||
["bash", str(SCRIPT), "--apply"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
timeout=12,
|
||||
)
|
||||
|
||||
assert result.returncode == 1
|
||||
_assert_ephemeral_resources_absent(Path(env["FAKE_DOCKER_STATE"]))
|
||||
status = _status(tmp_path)
|
||||
assert status["terminal"] == "failed"
|
||||
assert status["reason"] == "isolated_artifact_staging_failed"
|
||||
assert status["artifact_staging_verified"] is False
|
||||
assert status["cleanup_verified"] is True
|
||||
|
||||
|
||||
def test_staged_artifact_hash_mismatch_fails_before_isolated_services(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
artifact, manifest, inventory = _write_backup_fixture(tmp_path)
|
||||
restored = tmp_path / "restored.tsv"
|
||||
restored.write_text(_inventory(), encoding="utf-8")
|
||||
env = _environment(tmp_path, artifact, manifest, inventory, restored)
|
||||
assert _run("--check", env).returncode == 0
|
||||
env["FAKE_STAGING_HASH_MISMATCH"] = "1"
|
||||
|
||||
result = _run("--apply", env)
|
||||
|
||||
assert result.returncode == 1
|
||||
docker_lines = Path(env["FAKE_DOCKER_LOG"]).read_text(encoding="utf-8").splitlines()
|
||||
assert not any(
|
||||
"signoz/zookeeper:3.7.1" in line
|
||||
for line in docker_lines
|
||||
if line.startswith("run ")
|
||||
)
|
||||
_assert_ephemeral_resources_absent(Path(env["FAKE_DOCKER_STATE"]))
|
||||
status = _status(tmp_path)
|
||||
assert status["reason"] == "staged_artifact_hash_mismatch"
|
||||
assert status["cleanup_verified"] is True
|
||||
|
||||
|
||||
def test_clickhouse_artifact_hash_readback_mismatch_fails_and_cleans(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
artifact, manifest, inventory = _write_backup_fixture(tmp_path)
|
||||
restored = tmp_path / "restored.tsv"
|
||||
restored.write_text(_inventory(), encoding="utf-8")
|
||||
env = _environment(tmp_path, artifact, manifest, inventory, restored)
|
||||
assert _run("--check", env).returncode == 0
|
||||
env["FAKE_CLICKHOUSE_HASH_MISMATCH"] = "1"
|
||||
|
||||
result = _run("--apply", env)
|
||||
|
||||
assert result.returncode == 1
|
||||
_assert_ephemeral_resources_absent(Path(env["FAKE_DOCKER_STATE"]))
|
||||
status = _status(tmp_path)
|
||||
assert status["reason"] == "isolated_clickhouse_artifact_hash_mismatch"
|
||||
assert status["artifact_staging_verified"] is True
|
||||
assert status["clickhouse_artifact_readback_verified"] is False
|
||||
assert status["clickhouse_artifact_sha256"] == "f" * 64
|
||||
assert status["cleanup_verified"] is True
|
||||
|
||||
|
||||
def test_docker_daemon_failure_cannot_claim_cleanup_verified(tmp_path: Path) -> None:
|
||||
artifact, manifest, inventory = _write_backup_fixture(tmp_path)
|
||||
restored = tmp_path / "restored.tsv"
|
||||
restored.write_text(_inventory(), encoding="utf-8")
|
||||
env = _environment(tmp_path, artifact, manifest, inventory, restored)
|
||||
assert _run("--check", env).returncode == 0
|
||||
env["FAKE_DOCKER_INFO_FAIL"] = "1"
|
||||
|
||||
result = _run("--apply", env)
|
||||
|
||||
assert result.returncode == 1
|
||||
status = _status(tmp_path)
|
||||
assert status["terminal"] == "failed"
|
||||
assert status["cleanup_verified"] is False
|
||||
assert status["reason"] == "ephemeral_resource_cleanup_failed"
|
||||
|
||||
|
||||
def test_cleanup_label_inspect_hang_is_bounded_and_fails_closed(tmp_path: Path) -> None:
|
||||
artifact, manifest, inventory = _write_backup_fixture(tmp_path)
|
||||
restored = tmp_path / "restored.tsv"
|
||||
restored.write_text(_inventory(), encoding="utf-8")
|
||||
env = _environment(tmp_path, artifact, manifest, inventory, restored)
|
||||
env["CLICKHOUSE_RESTORE_COMMAND_TIMEOUT_SECONDS"] = "1"
|
||||
assert _run("--check", env).returncode == 0
|
||||
env["FAKE_CHECK_TABLE_FAIL"] = "1"
|
||||
env["FAKE_LABEL_INSPECT_HANG"] = "1"
|
||||
|
||||
result = subprocess.run(
|
||||
["bash", str(SCRIPT), "--apply"],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
timeout=12,
|
||||
)
|
||||
|
||||
assert result.returncode == 1
|
||||
status = _status(tmp_path)
|
||||
assert status["terminal"] == "failed"
|
||||
assert status["cleanup_verified"] is False
|
||||
assert status["reason"] == "ephemeral_resource_cleanup_failed"
|
||||
|
||||
|
||||
def test_check_fails_closed_on_local_image_id_drift(tmp_path: Path) -> None:
|
||||
artifact, manifest, inventory = _write_backup_fixture(tmp_path)
|
||||
restored = tmp_path / "restored.tsv"
|
||||
restored.write_text(_inventory(), encoding="utf-8")
|
||||
env = _environment(tmp_path, artifact, manifest, inventory, restored)
|
||||
env["FAKE_CLICKHOUSE_IMAGE_ID"] = "sha256:" + "0" * 64
|
||||
|
||||
result = _run("--check", env)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "clickhouse_local_image_id_mismatch" in result.stderr
|
||||
docker_log = Path(env["FAKE_DOCKER_LOG"]).read_text(encoding="utf-8")
|
||||
assert "network create" not in docker_log
|
||||
assert "volume create" not in docker_log
|
||||
|
||||
|
||||
def test_flat_host_deploy_accepts_reviewed_absolute_helper_and_config_paths(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
artifact, manifest, inventory = _write_backup_fixture(tmp_path)
|
||||
restored = tmp_path / "restored.tsv"
|
||||
restored.write_text(_inventory(), encoding="utf-8")
|
||||
env = _environment(tmp_path, artifact, manifest, inventory, restored)
|
||||
host_root = tmp_path / "host-backup"
|
||||
host_root.mkdir()
|
||||
helper = host_root / "clickhouse-restore-inventory.py"
|
||||
backup_config = host_root / "backup_disk.xml"
|
||||
cluster_config = host_root / "isolated-cluster.xml"
|
||||
shutil.copyfile(INVENTORY_HELPER, helper)
|
||||
shutil.copyfile(
|
||||
REPO_ROOT / "ops/signoz/clickhouse/config.d/backup_disk.xml", backup_config
|
||||
)
|
||||
shutil.copyfile(
|
||||
REPO_ROOT / "ops/signoz/clickhouse/restore-drill/config.d/isolated-cluster.xml",
|
||||
cluster_config,
|
||||
)
|
||||
env.update(
|
||||
{
|
||||
"CLICKHOUSE_RESTORE_INVENTORY_HELPER": str(helper),
|
||||
"CLICKHOUSE_RESTORE_BACKUP_DISK_CONFIG": str(backup_config),
|
||||
"CLICKHOUSE_RESTORE_CLUSTER_CONFIG": str(cluster_config),
|
||||
}
|
||||
)
|
||||
|
||||
result = _run("--check", env)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "terminal=check_pass_no_runtime_write" in result.stdout
|
||||
|
||||
|
||||
def test_inventory_compare_allows_online_row_delta_but_not_engine_drift(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source = tmp_path / "source.tsv"
|
||||
restored = tmp_path / "restored.tsv"
|
||||
source.write_text(_inventory(), encoding="utf-8")
|
||||
restored.write_text(_inventory(restored_delta=9), encoding="utf-8")
|
||||
|
||||
accepted = subprocess.run(
|
||||
[
|
||||
"python3",
|
||||
str(INVENTORY_HELPER),
|
||||
"compare",
|
||||
"--source-inventory",
|
||||
str(source),
|
||||
"--restored-inventory",
|
||||
str(restored),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
assert accepted.returncode == 0, accepted.stderr
|
||||
assert "manifest_parity=1" in accepted.stdout
|
||||
assert "row_delta=9" in accepted.stdout
|
||||
|
||||
restored.write_text(_inventory(engine_mismatch=True), encoding="utf-8")
|
||||
rejected = subprocess.run(
|
||||
[
|
||||
"python3",
|
||||
str(INVENTORY_HELPER),
|
||||
"compare",
|
||||
"--source-inventory",
|
||||
str(source),
|
||||
"--restored-inventory",
|
||||
str(restored),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
assert rejected.returncode == 1
|
||||
assert "table_engine_mismatch_signoz_meter_meter" in rejected.stderr
|
||||
|
||||
restored.write_text(_inventory(schema_mismatch=True), encoding="utf-8")
|
||||
schema_rejected = subprocess.run(
|
||||
[
|
||||
"python3",
|
||||
str(INVENTORY_HELPER),
|
||||
"compare",
|
||||
"--source-inventory",
|
||||
str(source),
|
||||
"--restored-inventory",
|
||||
str(restored),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
assert schema_rejected.returncode == 1
|
||||
assert "table_schema_mismatch_signoz_meter_meter" in schema_rejected.stderr
|
||||
528
scripts/backup/tests/test_signoz_backup_canary_wrapper.py
Normal file
528
scripts/backup/tests/test_signoz_backup_canary_wrapper.py
Normal file
@@ -0,0 +1,528 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
WRAPPER = ROOT / "scripts" / "backup" / "run-signoz-backup-canary.sh"
|
||||
PLAYBOOK = ROOT / "infra" / "ansible" / "playbooks" / "110-devops.yml"
|
||||
ANSIBLE_VALIDATE = ROOT / "scripts" / "ops" / "ansible-validate.sh"
|
||||
READINESS_AUDIT = (
|
||||
ROOT / "scripts" / "reboot-recovery" / "reboot-recovery-readiness-audit.sh"
|
||||
)
|
||||
|
||||
|
||||
def _write_executable(path: Path, text: str) -> None:
|
||||
path.write_text(text, encoding="utf-8")
|
||||
path.chmod(0o755)
|
||||
|
||||
|
||||
def _monitor_source(cooldown_dir: Path, *, omit_contract: bool = False) -> str:
|
||||
cooldown_log = (
|
||||
'log "COOLDOWN: ${container} skipped"'
|
||||
if not omit_contract
|
||||
else 'log "maintenance skip for ${container}"'
|
||||
)
|
||||
return f"""\
|
||||
#!/bin/bash
|
||||
: "${{ACTION_COOLDOWN_SECONDS:=${{SEND_COOLDOWN_SECONDS}}}}"
|
||||
: "${{COOLDOWN_DIR:={cooldown_dir}}}"
|
||||
: "${{EXCLUDE_CONTAINERS:=signoz-clickhouse}}"
|
||||
is_in_cooldown() {{
|
||||
local container="$1"
|
||||
local cooldown_file="${{COOLDOWN_DIR}}/${{container}}.cooldown"
|
||||
local last_sent now elapsed
|
||||
last_sent=$(cat "$cooldown_file")
|
||||
now=$(date +%s)
|
||||
elapsed=$(( now - last_sent ))
|
||||
if (( elapsed < ACTION_COOLDOWN_SECONDS )); then
|
||||
{cooldown_log}
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}}
|
||||
set_cooldown() {{ :; }}
|
||||
# A second monitor path may use the same elapsed expression. The wrapper must
|
||||
# validate the expression inside is_in_cooldown, not require global uniqueness.
|
||||
# elapsed=$(( now - last_sent ))
|
||||
while read -r container_name; do
|
||||
is_in_cooldown "$container_name" && continue
|
||||
set_cooldown "$container_name"
|
||||
container="$container_name"
|
||||
log "AUTO_REPAIR: docker restart ${{container}}"
|
||||
if docker restart "$container"; then
|
||||
:
|
||||
fi
|
||||
done
|
||||
"""
|
||||
|
||||
|
||||
def _install_fake_commands(fake_bin: Path) -> None:
|
||||
_write_executable(
|
||||
fake_bin / "crontab",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
[ "${1:-}" = "-l" ]
|
||||
printf '*/5 * * * * %s >> %s 2>&1\n' \
|
||||
"${TEST_MONITOR_SCRIPT:?}" "${TEST_MONITOR_LOG:?}"
|
||||
if [ "${FAKE_DUPLICATE_CRON:-0}" = "1" ]; then
|
||||
printf '*/5 * * * * %s >> %s 2>&1\n' \
|
||||
"${TEST_MONITOR_SCRIPT:?}" "${TEST_MONITOR_LOG:?}"
|
||||
fi
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
fake_bin / "docker",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
[ "${1:-}" = "inspect" ] || exit 70
|
||||
case " $* " in
|
||||
*"{{.State.StartedAt}}"*)
|
||||
printf 'sha256:collector-test-id\\t%s\\t2026-07-15T00:00:00Z\\t0\\thealthy\\n' \
|
||||
"$(cat "${TEST_COLLECTOR_STATE:?}")"
|
||||
;;
|
||||
*"{{.Id}}"*) printf 'sha256:collector-test-id\n' ;;
|
||||
*"{{.State.Running}}"*) cat "${TEST_COLLECTOR_STATE:?}" ;;
|
||||
*) exit 71 ;;
|
||||
esac
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
fake_bin / "ss",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
while IFS= read -r port; do
|
||||
[ -n "$port" ] || continue
|
||||
printf 'LISTEN 0 4096 *:%s *:*\n' "$port"
|
||||
done < "${TEST_LISTENERS:?}"
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
fake_bin / "timeout",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
while [[ "${1:-}" == --kill-after=* ]]; do
|
||||
shift
|
||||
done
|
||||
[ "$#" -gt 1 ]
|
||||
shift
|
||||
exec "$@"
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
fake_bin / "flock",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
fake_bin / "pgrep",
|
||||
"""\
|
||||
#!/bin/bash
|
||||
exit 1
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
fake_bin / "stat",
|
||||
"""\
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
|
||||
if len(sys.argv) != 4 or sys.argv[1] != "-c":
|
||||
raise SystemExit(64)
|
||||
value = os.stat(sys.argv[3])
|
||||
formats = {
|
||||
"%a": format(stat.S_IMODE(value.st_mode), "o"),
|
||||
"%u": str(value.st_uid),
|
||||
"%g": str(value.st_gid),
|
||||
"%s": str(value.st_size),
|
||||
"%d:%i": f"{value.st_dev}:{value.st_ino}",
|
||||
}
|
||||
if sys.argv[2] not in formats:
|
||||
raise SystemExit(65)
|
||||
print(formats[sys.argv[2]])
|
||||
""",
|
||||
)
|
||||
_write_executable(
|
||||
fake_bin / "sha256sum",
|
||||
"""\
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import sys
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit(64)
|
||||
with open(sys.argv[1], "rb") as source:
|
||||
digest = hashlib.sha256(source.read()).hexdigest()
|
||||
print(f"{digest} {sys.argv[1]}")
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def _backup_source() -> str:
|
||||
return """\
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
[ "${BACKUP_SKIP_RETENTION_CLEANUP:-}" = "1" ] || exit 80
|
||||
[ -n "${TRACE_ID:-}" ] && [ -n "${RUN_ID:-}" ] && [ -n "${WORK_ITEM_ID:-}" ]
|
||||
printf '%s\t%s\t%s\n' "$TRACE_ID" "$RUN_ID" "$WORK_ITEM_ID" \
|
||||
> "${TEST_BACKUP_IDENTITIES:?}"
|
||||
if [ "${FAKE_BACKUP_STOPS_COLLECTOR:-1}" = "1" ]; then
|
||||
cat "${TEST_COOLDOWN_FILE:?}" > "${TEST_LEASE_OBSERVED:?}"
|
||||
printf 'false\n' > "${TEST_COLLECTOR_STATE:?}"
|
||||
case "${FAKE_MONITOR_MODE:-cooldown}" in
|
||||
cooldown)
|
||||
printf '[test] DETECTED: signoz-otel-collector state=exited health=none\n' \
|
||||
>> "${TEST_MONITOR_LOG:?}"
|
||||
printf '[test] COOLDOWN: signoz-otel-collector skipped\n' \
|
||||
>> "${TEST_MONITOR_LOG:?}"
|
||||
;;
|
||||
auto_repair)
|
||||
printf '[test] DETECTED: signoz-otel-collector state=exited health=none\n' \
|
||||
>> "${TEST_MONITOR_LOG:?}"
|
||||
printf '[test] COOLDOWN: signoz-otel-collector skipped\n' \
|
||||
>> "${TEST_MONITOR_LOG:?}"
|
||||
printf '[test] AUTO_REPAIR: docker restart signoz-otel-collector\n' \
|
||||
>> "${TEST_MONITOR_LOG:?}"
|
||||
;;
|
||||
none) : ;;
|
||||
*) exit 81 ;;
|
||||
esac
|
||||
sleep "${FAKE_BACKUP_SLEEP_SECONDS:-0.2}"
|
||||
printf 'true\n' > "${TEST_COLLECTOR_STATE:?}"
|
||||
else
|
||||
sleep "${FAKE_BACKUP_SLEEP_SECONDS:-0.2}"
|
||||
fi
|
||||
if [ "${FAKE_DROP_LISTENER:-0}" = "1" ]; then
|
||||
printf '4317\n' > "${TEST_LISTENERS:?}"
|
||||
fi
|
||||
printf '略過 SignOz retention cleanup (BACKUP_SKIP_RETENTION_CLEANUP=1)\n'
|
||||
if [ "${FAKE_BACKUP_STOPS_COLLECTOR:-1}" = "1" ]; then
|
||||
printf '[SUCCESS] ========== SignOz 備份完成 (1s) ==========\n'
|
||||
else
|
||||
printf '[SUCCESS] ========== SigNoz ClickHouse native 備份完成 (1s) ==========\n'
|
||||
fi
|
||||
"""
|
||||
|
||||
|
||||
def _run_canary(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
mode: str = "apply",
|
||||
prior_lease: bool = True,
|
||||
monitor_mode: str = "cooldown",
|
||||
backup_sleep_seconds: float = 0.2,
|
||||
monitor_interval_seconds: int = 300,
|
||||
omit_monitor_contract: bool = False,
|
||||
duplicate_cron: bool = False,
|
||||
drop_listener: bool = False,
|
||||
expect_collector_stop: bool = True,
|
||||
) -> tuple[subprocess.CompletedProcess[str], dict[str, Path], str]:
|
||||
fake_bin = tmp_path / "bin"
|
||||
cooldown_dir = tmp_path / "cooldown"
|
||||
receipt_root = tmp_path / "receipts"
|
||||
fake_bin.mkdir()
|
||||
cooldown_dir.mkdir()
|
||||
receipt_root.mkdir()
|
||||
|
||||
monitor_script = tmp_path / "docker-health-monitor.sh"
|
||||
monitor_log = tmp_path / "monitor.log"
|
||||
backup_script = tmp_path / "backup-signoz.sh"
|
||||
collector_state = tmp_path / "collector.state"
|
||||
listeners = tmp_path / "listeners"
|
||||
identities = tmp_path / "backup-identities.tsv"
|
||||
lease_observed = tmp_path / "lease-observed.txt"
|
||||
cooldown_file = cooldown_dir / "signoz-otel-collector.cooldown"
|
||||
run_id = f"canary-{tmp_path.name}"
|
||||
|
||||
_write_executable(
|
||||
monitor_script,
|
||||
_monitor_source(cooldown_dir, omit_contract=omit_monitor_contract),
|
||||
)
|
||||
_write_executable(backup_script, _backup_source())
|
||||
_install_fake_commands(fake_bin)
|
||||
monitor_log.write_text("[test] monitor seed\n", encoding="utf-8")
|
||||
collector_state.write_text("true\n", encoding="utf-8")
|
||||
listeners.write_text("4317\n4318\n", encoding="utf-8")
|
||||
if prior_lease:
|
||||
cooldown_file.write_text("123\n", encoding="utf-8")
|
||||
cooldown_file.chmod(0o640)
|
||||
|
||||
env = {
|
||||
**os.environ,
|
||||
"PATH": f"{fake_bin}:{os.environ['PATH']}",
|
||||
"TRACE_ID": "trace-P0-OBS-002",
|
||||
"RUN_ID": run_id,
|
||||
"WORK_ITEM_ID": "P0-OBS-002",
|
||||
"SIGNOZ_CANARY_BACKUP_SCRIPT": str(backup_script),
|
||||
"SIGNOZ_CANARY_MONITOR_SCRIPT": str(monitor_script),
|
||||
"SIGNOZ_CANARY_MONITOR_LOG": str(monitor_log),
|
||||
"SIGNOZ_CANARY_COOLDOWN_DIR": str(cooldown_dir),
|
||||
"SIGNOZ_CANARY_RECEIPT_ROOT": str(receipt_root),
|
||||
"SIGNOZ_CANARY_LOCK_FILE": str(tmp_path / "canary.lock"),
|
||||
"SIGNOZ_CANARY_TIMEOUT_SECONDS": "5",
|
||||
"SIGNOZ_CANARY_KILL_AFTER_SECONDS": "2",
|
||||
"SIGNOZ_CANARY_LEASE_MARGIN_SECONDS": "2",
|
||||
"SIGNOZ_CANARY_MONITOR_CRON_INTERVAL_SECONDS": str(monitor_interval_seconds),
|
||||
"SIGNOZ_CANARY_STATE_POLL_SECONDS": "0.05",
|
||||
"SIGNOZ_CANARY_EXPECT_COLLECTOR_STOP": ("1" if expect_collector_stop else "0"),
|
||||
"TEST_MONITOR_SCRIPT": str(monitor_script),
|
||||
"TEST_MONITOR_LOG": str(monitor_log),
|
||||
"TEST_COLLECTOR_STATE": str(collector_state),
|
||||
"TEST_LISTENERS": str(listeners),
|
||||
"TEST_BACKUP_IDENTITIES": str(identities),
|
||||
"TEST_COOLDOWN_FILE": str(cooldown_file),
|
||||
"TEST_LEASE_OBSERVED": str(lease_observed),
|
||||
"FAKE_MONITOR_MODE": monitor_mode,
|
||||
"FAKE_BACKUP_SLEEP_SECONDS": str(backup_sleep_seconds),
|
||||
"FAKE_DUPLICATE_CRON": "1" if duplicate_cron else "0",
|
||||
"FAKE_DROP_LISTENER": "1" if drop_listener else "0",
|
||||
"FAKE_BACKUP_STOPS_COLLECTOR": "1" if expect_collector_stop else "0",
|
||||
}
|
||||
started_at = int(time.time())
|
||||
result = subprocess.run(
|
||||
["bash", str(WRAPPER), f"--{mode}"],
|
||||
env=env,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=15,
|
||||
check=False,
|
||||
)
|
||||
paths = {
|
||||
"cooldown": cooldown_file,
|
||||
"receipts": receipt_root / run_id / "receipts.jsonl",
|
||||
"rollback_metadata": receipt_root / run_id / "cooldown.rollback.json",
|
||||
"identities": identities,
|
||||
"lease_observed": lease_observed,
|
||||
"collector_state": collector_state,
|
||||
"listeners": listeners,
|
||||
}
|
||||
return result, paths, str(started_at)
|
||||
|
||||
|
||||
def _receipts(path: Path) -> list[dict[str, object]]:
|
||||
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()]
|
||||
|
||||
|
||||
def _terminal(receipts: list[dict[str, object]], stage: str) -> str:
|
||||
return str([item for item in receipts if item["stage"] == stage][-1]["terminal"])
|
||||
|
||||
|
||||
def test_existing_lease_is_atomically_replaced_and_exactly_restored(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, paths, started_at = _run_canary(tmp_path)
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert paths["cooldown"].read_text(encoding="utf-8") == "123\n"
|
||||
assert stat.S_IMODE(paths["cooldown"].stat().st_mode) == 0o640
|
||||
assert int(paths["lease_observed"].read_text(encoding="utf-8")) >= (
|
||||
int(started_at) + 7
|
||||
)
|
||||
identities = paths["identities"].read_text(encoding="utf-8").split("\t")
|
||||
assert identities[0] == "trace-P0-OBS-002"
|
||||
assert identities[1].startswith("canary-")
|
||||
assert identities[2] == "P0-OBS-002\n"
|
||||
receipts = _receipts(paths["receipts"])
|
||||
assert _terminal(receipts, "rollback") == "restored"
|
||||
assert _terminal(receipts, "terminal") == "pass"
|
||||
metadata = json.loads(paths["rollback_metadata"].read_text(encoding="utf-8"))
|
||||
assert metadata["trace_id"] == "trace-P0-OBS-002"
|
||||
assert metadata["prior_present"] == 1
|
||||
|
||||
stages = [str(item["stage"]) for item in receipts]
|
||||
ordered = [
|
||||
"sensor_source",
|
||||
"normalized_asset_identity",
|
||||
"source_of_truth_diff",
|
||||
"ai_decision",
|
||||
"risk_policy",
|
||||
"check",
|
||||
"execution",
|
||||
"post_verifier",
|
||||
"rollback",
|
||||
"closure_writeback",
|
||||
"terminal",
|
||||
]
|
||||
assert [stages.index(stage) for stage in ordered] == sorted(
|
||||
stages.index(stage) for stage in ordered
|
||||
)
|
||||
|
||||
|
||||
def test_absent_lease_is_removed_after_success(tmp_path: Path) -> None:
|
||||
result, paths, _ = _run_canary(tmp_path, prior_lease=False)
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert not paths["cooldown"].exists()
|
||||
receipts = _receipts(paths["receipts"])
|
||||
assert _terminal(receipts, "rollback") == "restored"
|
||||
assert _terminal(receipts, "terminal") == "pass"
|
||||
|
||||
|
||||
def test_online_native_canary_keeps_collector_running_and_does_not_arm_lease(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, paths, _ = _run_canary(tmp_path, expect_collector_stop=False)
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert paths["cooldown"].read_text(encoding="utf-8") == "123\n"
|
||||
assert not paths["lease_observed"].exists()
|
||||
assert paths["collector_state"].read_text(encoding="utf-8") == "true\n"
|
||||
receipts = _receipts(paths["receipts"])
|
||||
assert any(
|
||||
item["stage"] == "lease" and item["terminal"] == "not_required"
|
||||
for item in receipts
|
||||
)
|
||||
assert _terminal(receipts, "rollback") == "no_write"
|
||||
assert _terminal(receipts, "terminal") == "pass"
|
||||
|
||||
|
||||
def test_online_native_canary_is_not_coupled_to_legacy_monitor_contract(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, paths, _ = _run_canary(
|
||||
tmp_path,
|
||||
expect_collector_stop=False,
|
||||
omit_monitor_contract=True,
|
||||
duplicate_cron=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert not paths["lease_observed"].exists()
|
||||
receipts = _receipts(paths["receipts"])
|
||||
source_diff = [
|
||||
item for item in receipts if item["stage"] == "source_of_truth_diff"
|
||||
][-1]
|
||||
assert "monitor_and_cooldown_contract_not_applicable" in source_diff["detail"]
|
||||
assert _terminal(receipts, "terminal") == "pass"
|
||||
|
||||
|
||||
def test_auto_repair_contamination_fails_and_restores_lease(tmp_path: Path) -> None:
|
||||
result, paths, _ = _run_canary(tmp_path, monitor_mode="auto_repair")
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "monitor_auto_repair_contaminated_canary" in result.stderr
|
||||
assert paths["cooldown"].read_text(encoding="utf-8") == "123\n"
|
||||
receipts = _receipts(paths["receipts"])
|
||||
assert _terminal(receipts, "rollback") == "restored"
|
||||
assert _terminal(receipts, "terminal") == "failed"
|
||||
|
||||
|
||||
def test_cron_crossing_requires_monitor_cooldown_receipt(tmp_path: Path) -> None:
|
||||
result, paths, _ = _run_canary(
|
||||
tmp_path,
|
||||
monitor_mode="none",
|
||||
backup_sleep_seconds=1.2,
|
||||
monitor_interval_seconds=1,
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "cron_crossed_without_collector_cooldown_receipt" in result.stderr
|
||||
assert paths["cooldown"].read_text(encoding="utf-8") == "123\n"
|
||||
|
||||
|
||||
def test_cron_crossing_with_detected_and_cooldown_receipts_passes(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, paths, _ = _run_canary(
|
||||
tmp_path,
|
||||
monitor_mode="cooldown",
|
||||
backup_sleep_seconds=1.2,
|
||||
monitor_interval_seconds=1,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
receipts = _receipts(paths["receipts"])
|
||||
monitor_receipt = [
|
||||
item for item in receipts if item["stage"] == "monitor_verifier"
|
||||
][-1]
|
||||
assert monitor_receipt["terminal"] == "pass"
|
||||
assert "cron_crossed_1" in str(monitor_receipt["detail"])
|
||||
|
||||
|
||||
def test_missing_monitor_contract_fails_before_lease_or_backup(tmp_path: Path) -> None:
|
||||
result, paths, _ = _run_canary(tmp_path, omit_monitor_contract=True)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "monitor_contract_ambiguous_fragment_count_0" in result.stderr
|
||||
assert paths["cooldown"].read_text(encoding="utf-8") == "123\n"
|
||||
assert not paths["lease_observed"].exists()
|
||||
receipts = _receipts(paths["receipts"])
|
||||
assert _terminal(receipts, "rollback") == "not_armed"
|
||||
assert _terminal(receipts, "terminal") == "failed"
|
||||
|
||||
|
||||
def test_ambiguous_cron_contract_fails_before_lease_or_backup(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, paths, _ = _run_canary(tmp_path, duplicate_cron=True)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "monitor_cron_contract_ambiguous_count_2" in result.stderr
|
||||
assert paths["cooldown"].read_text(encoding="utf-8") == "123\n"
|
||||
assert not paths["lease_observed"].exists()
|
||||
|
||||
|
||||
def test_post_backup_listener_failure_fails_and_restores_lease(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, paths, _ = _run_canary(tmp_path, drop_listener=True)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "collector_post_backup_runtime_failed" in result.stderr
|
||||
assert paths["cooldown"].read_text(encoding="utf-8") == "123\n"
|
||||
receipts = _receipts(paths["receipts"])
|
||||
assert _terminal(receipts, "rollback") == "restored"
|
||||
assert _terminal(receipts, "terminal") == "failed"
|
||||
|
||||
|
||||
def test_wrapper_deployment_and_static_contracts_are_owned() -> None:
|
||||
wrapper = WRAPPER.read_text(encoding="utf-8")
|
||||
assert "BACKUP_SKIP_RETENTION_CLEANUP=1" in wrapper
|
||||
assert 'TRACE_ID="${TRACE_ID:-}"' in wrapper
|
||||
assert 'RUN_ID="${RUN_ID:-}"' in wrapper
|
||||
assert 'WORK_ITEM_ID="${WORK_ITEM_ID:-}"' in wrapper
|
||||
assert "monitor_auto_repair_contaminated_canary" in wrapper
|
||||
assert "collector_post_backup_runtime_failed" in wrapper
|
||||
assert WRAPPER.name in PLAYBOOK.read_text(encoding="utf-8")
|
||||
assert str(WRAPPER.relative_to(ROOT)) in ANSIBLE_VALIDATE.read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert str(WRAPPER.relative_to(ROOT)) in READINESS_AUDIT.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_check_mode_proves_contract_without_lease_or_backup_write(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
result, paths, _ = _run_canary(tmp_path, mode="check")
|
||||
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert paths["cooldown"].read_text(encoding="utf-8") == "123\n"
|
||||
assert stat.S_IMODE(paths["cooldown"].stat().st_mode) == 0o640
|
||||
assert not paths["identities"].exists()
|
||||
assert not paths["lease_observed"].exists()
|
||||
check_receipts = _receipts(paths["receipts"])
|
||||
assert _terminal(check_receipts, "rollback") == "no_write"
|
||||
assert _terminal(check_receipts, "terminal") == "check_pass_no_write"
|
||||
|
||||
|
||||
def test_wrapper_is_valid_bash() -> None:
|
||||
result = subprocess.run(
|
||||
["bash", "-n", str(WRAPPER)],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
Reference in New Issue
Block a user