修复自动备份真实挂载解析(07-27-automatic-offhost-backup)
这个提交包含在:
@@ -943,6 +943,10 @@ test "$(printf '%s\n' "$actual" | LC_ALL=C sort)" = \
|
|||||||
- Resolve one active Compose server/database pair and verify its build, PGDATA,
|
- Resolve one active Compose server/database pair and verify its build, PGDATA,
|
||||||
storage mount and database identity before backup. Do not use fixed container
|
storage mount and database identity before backup. Do not use fixed container
|
||||||
names or repository-relative runtime data.
|
names or repository-relative runtime data.
|
||||||
|
- Parse Docker mount identity from `docker inspect` JSON and require exactly one
|
||||||
|
destination match. Do not split human-formatted Go-template output on tabs or
|
||||||
|
spaces: `println` inserts argument spacing that can make a visually correct
|
||||||
|
line fail exact path comparison on a real daemon.
|
||||||
- Transfer only a complete `auto-<UTC timestamp>` three-file batch to ARM
|
- Transfer only a complete `auto-<UTC timestamp>` three-file batch to ARM
|
||||||
inbox. The forced key allows write-only `rrsync` or an exact finalize command;
|
inbox. The forced key allows write-only `rrsync` or an exact finalize command;
|
||||||
it rejects shell, sender/read and destructive rsync options.
|
it rejects shell, sender/read and destructive rsync options.
|
||||||
@@ -986,6 +990,9 @@ test "$(printf '%s\n' "$actual" | LC_ALL=C sort)" = \
|
|||||||
|
|
||||||
- Shell syntax plus `run-automatic-backup-contract.sh` using temporary files and
|
- Shell syntax plus `run-automatic-backup-contract.sh` using temporary files and
|
||||||
mocked Docker/SSH boundaries; it must never contact real hosts.
|
mocked Docker/SSH boundaries; it must never contact real hosts.
|
||||||
|
- Docker mocks must return the real `{{json .Mounts}}` shape consumed by the
|
||||||
|
runner. A simplified tab-delimited mock is not valid evidence for real mount
|
||||||
|
parsing.
|
||||||
- Focused WeCom alert tests for stdin path redaction, preserved AUTO_BACKUP
|
- Focused WeCom alert tests for stdin path redaction, preserved AUTO_BACKUP
|
||||||
issue identity, first/repeated failure and quarterly reminder behavior.
|
issue identity, first/repeated failure and quarterly reminder behavior.
|
||||||
- Environment contract assertions for all four scripts, three timers, explicit
|
- Environment contract assertions for all four scripts, three timers, explicit
|
||||||
|
|||||||
@@ -55,3 +55,10 @@
|
|||||||
- `scripts/check-minimal.sh` 通过:server 162 个测试文件、1004 项测试全部通过,client typecheck/build 通过。
|
- `scripts/check-minimal.sh` 通过:server 162 个测试文件、1004 项测试全部通过,client typecheck/build 通过。
|
||||||
- 环境合同、Trellis task/governance、敏感扫描、文档注册、流程合同和 `git diff --check` 均通过;文档注册保留既有 22 个未注册文件 warning,无本任务新增文档。
|
- 环境合同、Trellis task/governance、敏感扫描、文档注册、流程合同和 `git diff --check` 均通过;文档注册保留既有 22 个未注册文件 warning,无本任务新增文档。
|
||||||
- ARM 安装、生产 units、真实首批、timer enable、真实企微失败告警和第二人季度走查仍为外部门禁,未执行、不得写成已完成。
|
- ARM 安装、生产 units、真实首批、timer enable、真实企微失败告警和第二人季度走查仍为外部门禁,未执行、不得写成已完成。
|
||||||
|
|
||||||
|
### 首次远端执行增量
|
||||||
|
|
||||||
|
- ARM 已安装 rsync、密码锁定专用账号和 forced-command receiver;从生产验证普通 shell、sender/read、delete 均拒绝,写通道 dry-run 通过且未生成归档。
|
||||||
|
- 生产三组 units 已安装为 disabled/inactive,并绑定活动 Compose project、PGDATA、附件挂载、构建标识和固定 SSH 身份。
|
||||||
|
- 首次真实备份在 `runtime_identity` 阶段 fail-closed,未生成源端或 ARM 批次;真实 Docker `println` 输出的参数间空格导致制表符拆分后路径不精确相等。
|
||||||
|
- runner 已改为解析 `docker inspect` mount JSON 并要求唯一匹配;合成 mock 同步改为真实 JSON 形状。重新执行真实首批和 timer enable 尚待验证。
|
||||||
|
|||||||
@@ -326,11 +326,16 @@ for (const requiredFragment of [
|
|||||||
'write_success_state "$run_id" "$manifest_sha"',
|
'write_success_state "$run_id" "$manifest_sha"',
|
||||||
'prune_local_backups "$batch_name"',
|
'prune_local_backups "$batch_name"',
|
||||||
'--summary-file=-',
|
'--summary-file=-',
|
||||||
|
"docker inspect -f '{{json .Mounts}}'",
|
||||||
|
'if len(matches) != 1 or not matches[0]:',
|
||||||
]) {
|
]) {
|
||||||
if (!automaticBackupRunnerContent.includes(requiredFragment)) {
|
if (!automaticBackupRunnerContent.includes(requiredFragment)) {
|
||||||
errors.push(`run-automatic-backup.sh 缺少运行时安全合同: ${requiredFragment}`);
|
errors.push(`run-automatic-backup.sh 缺少运行时安全合同: ${requiredFragment}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (automaticBackupRunnerContent.includes('println .Destination "\\t" .Source')) {
|
||||||
|
errors.push('run-automatic-backup.sh 不得用人类可读 Go-template 分隔符解析真实 Docker mount');
|
||||||
|
}
|
||||||
|
|
||||||
for (const requiredFragment of [
|
for (const requiredFragment of [
|
||||||
'"$original_command" == rsync\\ --server\\ *',
|
'"$original_command" == rsync\\ --server\\ *',
|
||||||
|
|||||||
@@ -239,11 +239,11 @@ if [[ "${1:-}" == "inspect" ]]; then
|
|||||||
printf 'POSTGRES_DB=hyb_oa\nPOSTGRES_USER=backup_user\nPOSTGRES_PASSWORD=fixture\n'
|
printf 'POSTGRES_DB=hyb_oa\nPOSTGRES_USER=backup_user\nPOSTGRES_PASSWORD=fixture\n'
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
*Mounts*)
|
*json*.Mounts*)
|
||||||
if [[ "$container" == "server-id" ]]; then
|
if [[ "$container" == "server-id" ]]; then
|
||||||
printf '/app/storage\t%s\n' "$MOCK_STORAGE"
|
printf '[{"Destination":"/app/storage","Source":"%s"}]\n' "$MOCK_STORAGE"
|
||||||
else
|
else
|
||||||
printf '/var/lib/postgresql/data\t%s\n' "$MOCK_PG"
|
printf '[{"Destination":"/var/lib/postgresql/data","Source":"%s"}]\n' "$MOCK_PG"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
*Image*) printf 'sha256:%064d\n' 0 ;;
|
*Image*) printf 'sha256:%064d\n' 0 ;;
|
||||||
|
|||||||
@@ -134,8 +134,18 @@ container_env() {
|
|||||||
mount_source() {
|
mount_source() {
|
||||||
local container_id="$1"
|
local container_id="$1"
|
||||||
local destination="$2"
|
local destination="$2"
|
||||||
docker inspect -f '{{range .Mounts}}{{println .Destination "\t" .Source}}{{end}}' "$container_id" \
|
docker inspect -f '{{json .Mounts}}' "$container_id" \
|
||||||
| awk -F '\t' -v expected="$destination" '$1 == expected { print $2 }'
|
| python3 -c '
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
destination = sys.argv[1]
|
||||||
|
mounts = json.load(sys.stdin)
|
||||||
|
matches = [mount.get("Source", "") for mount in mounts if mount.get("Destination") == destination]
|
||||||
|
if len(matches) != 1 or not matches[0]:
|
||||||
|
raise SystemExit(1)
|
||||||
|
print(matches[0])
|
||||||
|
' "$destination"
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve_active_runtime() {
|
resolve_active_runtime() {
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户