From 9875bc37a4ba790e696f1c95e55b386879c00f70 Mon Sep 17 00:00:00 2001 From: shunli Date: Mon, 27 Jul 2026 11:19:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=87=AA=E5=8A=A8=E5=A4=87?= =?UTF-8?q?=E4=BB=BD=E7=9C=9F=E5=AE=9E=E6=8C=82=E8=BD=BD=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=EF=BC=8807-27-automatic-offhost-backup=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trellis/spec/hyb-oa/deployment-trialrun.md | 7 +++++++ .../07-27-automatic-offhost-backup/implement.md | 7 +++++++ deploy/trialrun/validate-env-contract.js | 5 +++++ scripts/qa/run-automatic-backup-contract.sh | 6 +++--- scripts/run-automatic-backup.sh | 14 ++++++++++++-- 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/.trellis/spec/hyb-oa/deployment-trialrun.md b/.trellis/spec/hyb-oa/deployment-trialrun.md index 9db9d02c..11c2d59a 100644 --- a/.trellis/spec/hyb-oa/deployment-trialrun.md +++ b/.trellis/spec/hyb-oa/deployment-trialrun.md @@ -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, storage mount and database identity before backup. Do not use fixed container 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-` three-file batch to ARM inbox. The forced key allows write-only `rrsync` or an exact finalize command; 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 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 issue identity, first/repeated failure and quarterly reminder behavior. - Environment contract assertions for all four scripts, three timers, explicit diff --git a/.trellis/tasks/07-27-automatic-offhost-backup/implement.md b/.trellis/tasks/07-27-automatic-offhost-backup/implement.md index b3e4c12a..a5d8a3ce 100644 --- a/.trellis/tasks/07-27-automatic-offhost-backup/implement.md +++ b/.trellis/tasks/07-27-automatic-offhost-backup/implement.md @@ -55,3 +55,10 @@ - `scripts/check-minimal.sh` 通过:server 162 个测试文件、1004 项测试全部通过,client typecheck/build 通过。 - 环境合同、Trellis task/governance、敏感扫描、文档注册、流程合同和 `git diff --check` 均通过;文档注册保留既有 22 个未注册文件 warning,无本任务新增文档。 - 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 尚待验证。 diff --git a/deploy/trialrun/validate-env-contract.js b/deploy/trialrun/validate-env-contract.js index d12b56b3..272f0b2d 100755 --- a/deploy/trialrun/validate-env-contract.js +++ b/deploy/trialrun/validate-env-contract.js @@ -326,11 +326,16 @@ for (const requiredFragment of [ 'write_success_state "$run_id" "$manifest_sha"', 'prune_local_backups "$batch_name"', '--summary-file=-', + "docker inspect -f '{{json .Mounts}}'", + 'if len(matches) != 1 or not matches[0]:', ]) { if (!automaticBackupRunnerContent.includes(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 [ '"$original_command" == rsync\\ --server\\ *', diff --git a/scripts/qa/run-automatic-backup-contract.sh b/scripts/qa/run-automatic-backup-contract.sh index 7da8c7ad..f654786d 100755 --- a/scripts/qa/run-automatic-backup-contract.sh +++ b/scripts/qa/run-automatic-backup-contract.sh @@ -239,11 +239,11 @@ if [[ "${1:-}" == "inspect" ]]; then printf 'POSTGRES_DB=hyb_oa\nPOSTGRES_USER=backup_user\nPOSTGRES_PASSWORD=fixture\n' fi ;; - *Mounts*) + *json*.Mounts*) if [[ "$container" == "server-id" ]]; then - printf '/app/storage\t%s\n' "$MOCK_STORAGE" + printf '[{"Destination":"/app/storage","Source":"%s"}]\n' "$MOCK_STORAGE" else - printf '/var/lib/postgresql/data\t%s\n' "$MOCK_PG" + printf '[{"Destination":"/var/lib/postgresql/data","Source":"%s"}]\n' "$MOCK_PG" fi ;; *Image*) printf 'sha256:%064d\n' 0 ;; diff --git a/scripts/run-automatic-backup.sh b/scripts/run-automatic-backup.sh index ffb7d326..d3ed75ce 100755 --- a/scripts/run-automatic-backup.sh +++ b/scripts/run-automatic-backup.sh @@ -134,8 +134,18 @@ container_env() { mount_source() { local container_id="$1" local destination="$2" - docker inspect -f '{{range .Mounts}}{{println .Destination "\t" .Source}}{{end}}' "$container_id" \ - | awk -F '\t' -v expected="$destination" '$1 == expected { print $2 }' + docker inspect -f '{{json .Mounts}}' "$container_id" \ + | 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() {