mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): rehearse exact local upgrade cutover
This commit is contained in:
@@ -654,6 +654,12 @@ jobs:
|
|||||||
--pids-limit=32 \
|
--pids-limit=32 \
|
||||||
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
|
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
|
||||||
"${OPERATOR_IMAGE}" setup --help
|
"${OPERATOR_IMAGE}" setup --help
|
||||||
|
docker run --rm --read-only \
|
||||||
|
--network none \
|
||||||
|
--cap-drop ALL \
|
||||||
|
--security-opt no-new-privileges \
|
||||||
|
--entrypoint /usr/bin/docker \
|
||||||
|
"${OPERATOR_IMAGE}" --version
|
||||||
- name: Build the short-lived fresh deployment authority
|
- name: Build the short-lived fresh deployment authority
|
||||||
working-directory: packages/ql3-local-owner-cli
|
working-directory: packages/ql3-local-owner-cli
|
||||||
run: node ../../scripts/ql3-build-package-closure.cjs
|
run: node ../../scripts/ql3-build-package-closure.cjs
|
||||||
@@ -777,16 +783,35 @@ jobs:
|
|||||||
"${UPGRADE_EVIDENCE_ROOT}/results/sqlite-inspect.result.json")"
|
"${UPGRADE_EVIDENCE_ROOT}/results/sqlite-inspect.result.json")"
|
||||||
DIRECTORY_PLAN_DIGEST="$(node -e 'process.stdout.write(JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).evidence.planDigest)' \
|
DIRECTORY_PLAN_DIGEST="$(node -e 'process.stdout.write(JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).evidence.planDigest)' \
|
||||||
"${UPGRADE_EVIDENCE_ROOT}/results/data-directory-inspect.result.json")"
|
"${UPGRADE_EVIDENCE_ROOT}/results/data-directory-inspect.result.json")"
|
||||||
UPGRADE_REHEARSAL_ROOT="${RUNNER_TEMP}/ql3-alpha-upgrade-rehearsal-${{ matrix.image_arch }}"
|
UPGRADE_REHEARSAL_ROOT="${RUNNER_TEMP}/ql3-alpha-upgrade-cutover-rehearsal-${{ matrix.image_arch }}"
|
||||||
sh "${BUNDLE_ROOT}/upgrade-rehearsal.sh" \
|
LEGACY_CONTAINER="ql3-alpha-${TRIAL_VARIANT}-upgrade-legacy-${{ matrix.image_arch }}"
|
||||||
edge "${LEGACY_READINESS_ROOT}" "${UPGRADE_REHEARSAL_ROOT}" \
|
TARGET_CONTAINER="ql3-alpha-${TRIAL_VARIANT}-upgrade-target-${{ matrix.image_arch }}"
|
||||||
"${SQLITE_PLAN_DIGEST}" "${DIRECTORY_PLAN_DIGEST}"
|
trap 'docker rm --force "${QUICKSTART_CONTAINER}" "${TARGET_CONTAINER}" "${LEGACY_CONTAINER}" >/dev/null 2>&1 || true' EXIT
|
||||||
|
if [[ "${TRIAL_VARIANT}" == 'headless' ]]; then
|
||||||
|
sh "${BUNDLE_ROOT}/upgrade-cutover-rehearsal.sh" \
|
||||||
|
edge "${LEGACY_READINESS_ROOT}" "${UPGRADE_REHEARSAL_ROOT}" \
|
||||||
|
"${SQLITE_PLAN_DIGEST}" "${DIRECTORY_PLAN_DIGEST}" \
|
||||||
|
"${LEGACY_CONTAINER}" "${TARGET_CONTAINER}"
|
||||||
|
else
|
||||||
|
sh "${BUNDLE_ROOT}/upgrade-rehearsal.sh" \
|
||||||
|
edge "${LEGACY_READINESS_ROOT}" "${UPGRADE_REHEARSAL_ROOT}" \
|
||||||
|
"${SQLITE_PLAN_DIGEST}" "${DIRECTORY_PLAN_DIGEST}"
|
||||||
|
fi
|
||||||
grep -q '"status":"verified"' \
|
grep -q '"status":"verified"' \
|
||||||
"${UPGRADE_REHEARSAL_ROOT}/stage-summary.json"
|
"${UPGRADE_REHEARSAL_ROOT}/stage-summary.json"
|
||||||
grep -q '"legacySource":"read_only"' \
|
grep -q '"legacySource":"read_only"' \
|
||||||
"${UPGRADE_REHEARSAL_ROOT}/stage-summary.json"
|
"${UPGRADE_REHEARSAL_ROOT}/stage-summary.json"
|
||||||
grep -q '"cutover":"not_authorized"' \
|
grep -q '"cutover":"not_authorized"' \
|
||||||
"${UPGRADE_REHEARSAL_ROOT}/stage-summary.json"
|
"${UPGRADE_REHEARSAL_ROOT}/stage-summary.json"
|
||||||
|
if [[ "${TRIAL_VARIANT}" == 'headless' ]]; then
|
||||||
|
grep -q '"status":"rollback_candidate"' \
|
||||||
|
"${UPGRADE_REHEARSAL_ROOT}/cutover-summary.json"
|
||||||
|
grep -q '"legacySource":"unchanged"' \
|
||||||
|
"${UPGRADE_REHEARSAL_ROOT}/cutover-summary.json"
|
||||||
|
grep -q '"target":"stopped"' \
|
||||||
|
"${UPGRADE_REHEARSAL_ROOT}/cutover-summary.json"
|
||||||
|
docker rm "${TARGET_CONTAINER}" "${LEGACY_CONTAINER}"
|
||||||
|
fi
|
||||||
test ! -e "${LEGACY_READINESS_ROOT}/db/database.sqlite-wal"
|
test ! -e "${LEGACY_READINESS_ROOT}/db/database.sqlite-wal"
|
||||||
test ! -e "${LEGACY_READINESS_ROOT}/db/database.sqlite-journal"
|
test ! -e "${LEGACY_READINESS_ROOT}/db/database.sqlite-journal"
|
||||||
trap - EXIT
|
trap - EXIT
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ RUN rm -rf node_modules/.bin \
|
|||||||
FROM node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436 AS runtime
|
FROM node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436 AS runtime
|
||||||
|
|
||||||
RUN apk add --no-cache --upgrade \
|
RUN apk add --no-cache --upgrade \
|
||||||
|
docker-cli=29.5.2-r0 \
|
||||||
libcrypto3=3.5.8-r0 \
|
libcrypto3=3.5.8-r0 \
|
||||||
libssl3=3.5.8-r0
|
libssl3=3.5.8-r0
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# ADR-0524:Exact Headless 升级切换链演练
|
||||||
|
|
||||||
|
- 状态:Accepted(D-426b2b 源码候选;双架构阶段实物待本次 artifact run)
|
||||||
|
- 日期:2026-08-30
|
||||||
|
- 决策:D-426b2b
|
||||||
|
- 关联:ADR-0520、ADR-0521、ADR-0522、ADR-0523
|
||||||
|
|
||||||
|
## 上下文
|
||||||
|
|
||||||
|
ADR-0523 已修正 Apply 后 Adopted Target 的回退基线,但仓库内单元测试不能证明用户下载到的 Docker archive 具备完整控制器能力。阶段产物必须从将要上传的 exact bundle 走通 reviewed stage、Owner 认证、apply、真实 Docker legacy stop、target start/stop 和 clean `rollback_candidate`。
|
||||||
|
|
||||||
|
短生命周期 Operator 此前不含 Docker client,无法在容器内通过受约束的 Docker socket 执行既有 controller。另一方面,Console 镜像入口是 Local API,而 adopted target 证据当前只接受 `local-application-process@v3/v4`;把 Console fresh journey 标记为已验证切换会形成错误承诺。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
1. Operator runtime 增加固定 Alpine 版本的 `docker-cli`,不包含 daemon、Compose、listener、timer 或常驻进程;现有无网络、只读 rootfs、128 MiB、0.5 CPU、32 PID 边界不变。
|
||||||
|
2. Trial Kit v8 新增 canonical `upgrade-cutover-rehearsal.sh`,并将其文件摘要绑定到 bundle manifest、offline auditor 与 Local milestone v5。
|
||||||
|
3. 脚本仅允许 headless Trial Kit,在全新 rehearsal root 和两个专用合成容器上执行:
|
||||||
|
- exact bundle checksum/load/image identity;
|
||||||
|
- reviewed SQLite 与 data-directory stage/verify/activation;
|
||||||
|
- fresh Owner ceremony、credential presentation、transform/apply/verify;
|
||||||
|
- Docker socket 上的 legacy stop、offline image target start/stop;
|
||||||
|
- Application v4 post-apply baseline 与最终 `rollback_candidate`。
|
||||||
|
4. Legacy root 在 Operator、合成 Legacy 与 target 中均为只读绑定;演练前后主 SQLite SHA-256 必须一致,WAL/journal 不得出现。
|
||||||
|
5. 成功后写入私有 `cutover-summary.json`,固定 `status=rollback_candidate`、`legacySource=unchanged`、`target=stopped`;CI 删除两个已停止合成容器后才可上传产物。
|
||||||
|
6. Console 继续执行 exact readiness/stage,`legacyUpgradeCutover=not_applicable`。在 target evidence 支持 Local API 入口前,不得将其写成 `passed`。
|
||||||
|
|
||||||
|
## 产物闭包
|
||||||
|
|
||||||
|
- `qinglong/alpha-local-trial-kit@v8` / verification v6 / audit v5;
|
||||||
|
- `qinglong/alpha-local-milestone@v5` / audit v5;
|
||||||
|
- Stage Index v2 只接受新的 Local milestone v5;
|
||||||
|
- headless 原生 amd64、arm64 artifact job 必须执行 exact cutover rehearsal;普通源码 CI、模拟 Docker 或单架构结果都不能替代该门。
|
||||||
|
|
||||||
|
本 ADR 不授权生产 cutover、真实 2.x 容器停机、Legacy restart/rollback 或数据目录替换。新阶段实物只有在同源双架构 artifact 与 milestone 均由成功终态 workflow 闭合后成立。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- back:1657 total / 1655 pass / 2 conditional skip / 0 fail;
|
||||||
|
- Local Owner CLI:308 total / 301 pass / 7 conditional skip / 0 fail;
|
||||||
|
- Trial Kit、Local milestone、Stage Index 聚焦测试:29/29;
|
||||||
|
- package boundary 保持 18 个 workspace package、无 single/shallow package、零 finding;
|
||||||
|
- Operator image、milestone workflow、Stage Index workflow auditor 均为 compatible。
|
||||||
|
|
||||||
|
真实镜像构建、固定 Docker CLI、双架构 exact rehearsal 和新 artifact 摘要将在本提交的 GitHub artifact run 中补充。只有该 run 成功后,才把状态更新为“阶段实物已闭合”。
|
||||||
|
|
||||||
|
## 后续
|
||||||
|
|
||||||
|
D-426b2c 评估 Console adopted target 的显式双进程/入口证据模型;D-426c 继续处理 target 写入后的 capture、review、reconciliation 与恢复。两者都不得削弱 headless 已闭合的离线镜像和回退基线。
|
||||||
@@ -526,6 +526,8 @@
|
|||||||
| [ADR-0520](./ADR-0520-downloadable-local-legacy-upgrade-readiness.md) | 可下载的 Local Legacy 升级就绪盘点 | Accepted(D-425 双架构 Alpha 实物已交付) |
|
| [ADR-0520](./ADR-0520-downloadable-local-legacy-upgrade-readiness.md) | 可下载的 Local Legacy 升级就绪盘点 | Accepted(D-425 双架构 Alpha 实物已交付) |
|
||||||
| [ADR-0521](./ADR-0521-reviewed-side-by-side-local-upgrade-stage.md) | 受审核计划驱动的 Local Side-by-side 升级暂存 | Accepted(D-426a 双架构 Alpha 实物已交付) |
|
| [ADR-0521](./ADR-0521-reviewed-side-by-side-local-upgrade-stage.md) | 受审核计划驱动的 Local Side-by-side 升级暂存 | Accepted(D-426a 双架构 Alpha 实物已交付) |
|
||||||
| [ADR-0522](./ADR-0522-content-bound-offline-docker-adopted-target.md) | 内容绑定的离线 Docker Adopted Target | Accepted(D-426b 源码候选;阶段实物尚未闭合) |
|
| [ADR-0522](./ADR-0522-content-bound-offline-docker-adopted-target.md) | 内容绑定的离线 Docker Adopted Target | Accepted(D-426b 源码候选;阶段实物尚未闭合) |
|
||||||
|
| [ADR-0523](./ADR-0523-post-apply-adopted-target-baseline.md) | Apply 后的 Adopted Target 启动前基线 | Accepted(D-426b2a 源码候选;阶段实物尚未闭合) |
|
||||||
|
| [ADR-0524](./ADR-0524-exact-headless-upgrade-cutover-rehearsal.md) | Exact Headless 升级切换链演练 | Accepted(D-426b2b 源码候选;双架构阶段实物待闭合) |
|
||||||
|
|
||||||
## 规则
|
## 规则
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ ADR-0506 的 `qinglong/alpha-local-trial-kit@v2` 首次增加 source-bound verif
|
|||||||
Local artifact 含:
|
Local artifact 含:
|
||||||
|
|
||||||
- 一个包含所选 Application 与短生命周期 operator 的 archive;headless 为 `qinglong3-local-trial-kit-<arch>.docker.tar`,Console 为 `qinglong3-local-console-trial-kit-<arch>.docker.tar`,共享 Node 基础层在 archive 中去重;
|
- 一个包含所选 Application 与短生命周期 operator 的 archive;headless 为 `qinglong3-local-trial-kit-<arch>.docker.tar`,Console 为 `qinglong3-local-console-trial-kit-<arch>.docker.tar`,共享 Node 基础层在 archive 中去重;
|
||||||
- schema 为 `qinglong/alpha-local-trial-kit@v7` 的 `manifest.json`,通过 `variant/archive/images/sboms/quickstart/upgradeReadiness/upgradeRehearsal/readme/verification` 绑定版本、完整 source commit、架构、两个 image tag/image ID 与文件长度/SHA-256;
|
- schema 为 `qinglong/alpha-local-trial-kit@v8` 的 `manifest.json`,通过 `variant/archive/images/sboms/quickstart/upgradeReadiness/upgradeRehearsal/upgradeCutoverRehearsal/readme/verification` 绑定版本、完整 source commit、架构、两个 image tag/image ID 与文件长度/SHA-256;
|
||||||
- canonical `quickstart.sh`,在目标 Linux 设备上只依赖 POSIX shell、`sha256sum` 和 Docker,完成 checksum、load、identity、fresh Owner 与 Profile-bound Application active;
|
- canonical `quickstart.sh`,在目标 Linux 设备上只依赖 POSIX shell、`sha256sum` 和 Docker,完成 checksum、load、identity、fresh Owner 与 Profile-bound Application active;
|
||||||
- canonical `upgrade-readiness.sh`,把 2.x data root 只读挂载给 128 MiB/无网络 Operator,生成 SQLite 与完整目录两个私有 inspect 计划,不获得 stage/cutover authority;
|
- canonical `upgrade-readiness.sh`,把 2.x data root 只读挂载给 128 MiB/无网络 Operator,生成 SQLite 与完整目录两个私有 inspect 计划,不获得 stage/cutover authority;
|
||||||
- `verification-evidence.json` 绑定 `workflow_dispatch` 的 workflow ref/SHA、run ID/attempt、同架构两个 exact image ID 和完整 gate 集;下载者仍须到 GitHub 交叉检查 run,它不替代正式签名;
|
- `verification-evidence.json` 绑定 `workflow_dispatch` 的 workflow ref/SHA、run ID/attempt、同架构两个 exact image ID 和完整 gate 集;下载者仍须到 GitHub 交叉检查 run,它不替代正式签名;
|
||||||
@@ -119,7 +119,7 @@ Local artifact 含:
|
|||||||
|
|
||||||
Cluster artifact 是每角色/架构一个六文件闭包:native Docker archive、精确 CycloneDX SBOM、workflow-bound verification evidence、README、`qinglong/alpha-cluster-image@v1` manifest 和覆盖全部内容文件的 `SHA256SUMS`。完整 CI 成功后,八个 bundle 由 `qinglong/alpha-cluster-milestone@v1` 小型索引闭合;索引本身不重复存放大 archive。
|
Cluster artifact 是每角色/架构一个六文件闭包:native Docker archive、精确 CycloneDX SBOM、workflow-bound verification evidence、README、`qinglong/alpha-cluster-image@v1` manifest 和覆盖全部内容文件的 `SHA256SUMS`。完整 CI 成功后,八个 bundle 由 `qinglong/alpha-cluster-milestone@v1` 小型索引闭合;索引本身不重复存放大 archive。
|
||||||
|
|
||||||
Local milestone 是 `qinglong/alpha-local-milestone@v4` 三文件闭包,绑定一个 variant 的双架构 Trial Kit,并直接记录两个架构的 `upgradeReadinessSha256` 与 `upgradeRehearsalSha256`。Stage index 是 `qinglong/alpha-stage-index@v2` 三文件闭包;它重新审计两个 milestone,要求 version/source/workflow SHA/ref/run/attempt 一致,并把 Local variant/Profile 与 Cluster 的 control/admin/worker 最小集、可选 control-ai 写为机器可读选择;它不重复存放任何镜像 archive。
|
Local milestone 是 `qinglong/alpha-local-milestone@v5` 三文件闭包,绑定一个 variant 的双架构 Trial Kit,并直接记录两个架构的 `upgradeReadinessSha256`、`upgradeRehearsalSha256` 与 `upgradeCutoverRehearsalSha256`。Stage index 是 `qinglong/alpha-stage-index@v2` 三文件闭包;它重新审计两个 milestone,要求 version/source/workflow SHA/ref/run/attempt 一致,并把 Local variant/Profile 与 Cluster 的 control/admin/worker 最小集、可选 control-ai 写为机器可读选择;它不重复存放任何镜像 archive。
|
||||||
|
|
||||||
任何 required job 失败时不上传对应产物。artifact 名和 archive 内的 `ci-*` tag 都表示 commit-bound candidate,不能改名后冒充 `v3.x` release。
|
任何 required job 失败时不上传对应产物。artifact 名和 archive 内的 `ci-*` tag 都表示 commit-bound candidate,不能改名后冒充 `v3.x` release。
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. 打开 `manifest.json`,确认:
|
2. 打开 `manifest.json`,确认:
|
||||||
- `schema` 为 `qinglong/alpha-local-milestone@v4`;
|
- `schema` 为 `qinglong/alpha-local-milestone@v5`;
|
||||||
- `variant` 为 `headless` 或 `console`,且两个架构记录都使用同一变体;
|
- `variant` 为 `headless` 或 `console`,且两个架构记录都使用同一变体;
|
||||||
- `sourceRevision` 是准备试用的完整 40 位提交;
|
- `sourceRevision` 是准备试用的完整 40 位提交;
|
||||||
- `workflow.event` 为 `workflow_dispatch`,`workflow.job` 为 `local-alpha-milestone`;
|
- `workflow.event` 为 `workflow_dispatch`,`workflow.job` 为 `local-alpha-milestone`;
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
- `artifacts` 恰好包含 `amd64` 与 `arm64`。
|
- `artifacts` 恰好包含 `amd64` 与 `arm64`。
|
||||||
3. 根据主机架构下载 `artifacts.<architecture>.artifactName` 指向的 Trial Kit。
|
3. 根据主机架构下载 `artifacts.<architecture>.artifactName` 指向的 Trial Kit。
|
||||||
4. 对 Trial Kit 先执行其 `SHA256SUMS`,再确认其中 `manifest.json` 的 SHA-256 与 milestone 的 `bundleManifest.sha256` 完全一致。
|
4. 对 Trial Kit 先执行其 `SHA256SUMS`,再确认其中 `manifest.json` 的 SHA-256 与 milestone 的 `bundleManifest.sha256` 完全一致。
|
||||||
5. 确认 milestone 的 `upgradeReadinessSha256`、`upgradeRehearsalSha256` 与 Trial Kit manifest 中同名入口摘要一致,再按 Trial Kit 自带 `README.md` 完成 fresh smoke、只读 2.x 升级就绪盘点或受审核计划的 side-by-side 暂存。
|
5. 确认 milestone 的 `upgradeReadinessSha256`、`upgradeRehearsalSha256`、`upgradeCutoverRehearsalSha256` 与 Trial Kit manifest 中同名入口摘要一致,再按 Trial Kit 自带 `README.md` 完成 fresh smoke、只读 2.x 升级就绪盘点、受审核计划的 side-by-side 暂存或隔离切换链演练。
|
||||||
|
|
||||||
若持有同一版本源码与 Node.js 24,可额外审计 milestone 索引:
|
若持有同一版本源码与 Node.js 24,可额外审计 milestone 索引:
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ sha256sum --check SHA256SUMS
|
|||||||
|
|
||||||
`manifest.json` 必须满足:
|
`manifest.json` 必须满足:
|
||||||
|
|
||||||
- `schema` 为 `qinglong/alpha-local-trial-kit@v7`;
|
- `schema` 为 `qinglong/alpha-local-trial-kit@v8`;
|
||||||
- `variant` 为 `headless` 或 `console`,并与 milestone、application SBOM 和 artifact 名一致;
|
- `variant` 为 `headless` 或 `console`,并与 milestone、application SBOM 和 artifact 名一致;
|
||||||
- `sourceRevision` 是你准备试用的完整 40 位 commit;
|
- `sourceRevision` 是你准备试用的完整 40 位 commit;
|
||||||
- `architecture` 与主机相同;
|
- `architecture` 与主机相同;
|
||||||
@@ -116,7 +116,7 @@ artifact job 必须在原生 amd64/arm64 上使用生产形态 2.x fixture 运
|
|||||||
|
|
||||||
## 受审核计划的 Side-by-side 暂存
|
## 受审核计划的 Side-by-side 暂存
|
||||||
|
|
||||||
审核上一节两个完整结果后,把其中 exact `evidence.planDigest` 作为显式参数交给 v7 bundle 的 canonical `upgrade-rehearsal.sh`:
|
审核上一节两个完整结果后,把其中 exact `evidence.planDigest` 作为显式参数交给 v8 bundle 的 canonical `upgrade-rehearsal.sh`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sh upgrade-rehearsal.sh \
|
sh upgrade-rehearsal.sh \
|
||||||
@@ -135,6 +135,27 @@ staging manifest。summary 必须是 `status=verified`、`legacySource=read_only
|
|||||||
复用或当作生产数据根;后续 adopted start 必须精确消费这里的 evidence,并走独立的 D-426b 门。artifact job 必须对将要上传的 exact 脚本使用同一个
|
复用或当作生产数据根;后续 adopted start 必须精确消费这里的 evidence,并走独立的 D-426b 门。artifact job 必须对将要上传的 exact 脚本使用同一个
|
||||||
生产形态 fixture 实跑,并记录 `verification-evidence.json.gates.legacyUpgradeStage=passed`。
|
生产形态 fixture 实跑,并记录 `verification-evidence.json.gates.legacyUpgradeStage=passed`。
|
||||||
|
|
||||||
|
## 隔离的真实切换链演练
|
||||||
|
|
||||||
|
v8 headless bundle 进一步提供 `upgrade-cutover-rehearsal.sh`。它只面向 Linux Docker 测试主机,在新的 rehearsal root 和两个专用合成容器上消费上一阶段已审核的两个 plan digest:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh upgrade-cutover-rehearsal.sh \
|
||||||
|
edge \
|
||||||
|
/opt/qinglong/data \
|
||||||
|
/opt/qinglong3-alpha-upgrade-cutover \
|
||||||
|
<reviewed-sqlite-plan-digest> \
|
||||||
|
<reviewed-data-directory-plan-digest> \
|
||||||
|
ql3-alpha-upgrade-legacy \
|
||||||
|
ql3-alpha-upgrade-target
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本先重跑 canonical stage/verify,再完成 fresh Owner 建立、data-directory transform/apply、真实 Docker socket 上的合成 Legacy 停机和 3.0 target 启停。Operator 镜像仅增加固定版本 Docker CLI,仍不携带 daemon、Compose,也不常驻。Legacy root 在所有容器中均以只读方式挂载;脚本对演练前后的 `db/database.sqlite` 做 SHA-256 闭合校验。
|
||||||
|
|
||||||
|
成功时 `cutover-summary.json` 必须同时为 `status=rollback_candidate`、`legacySource=unchanged`、`target=stopped`。两个合成容器会保持停止状态供审查,随后按脚本输出显式 `docker rm`;失败时脚本自动清理。该结果证明打包产物能够走通控制器链和 Docker 证据闭环,但不会停止用户真实 2.x 容器、执行 Legacy restart/rollback 或授权生产升级。
|
||||||
|
|
||||||
|
原生 amd64/arm64 headless artifact job 必须从将要上传的目录执行 exact `upgrade-cutover-rehearsal.sh`,检查 summary 和旧 SQLite 未变,并删除合成容器后才能上传;对应 gate 为 `verification-evidence.json.gates.legacyUpgradeCutover=passed`。Console artifact 继续实跑 canonical stage,但该 gate 固定为 `not_applicable`;在 adopted target 证据正式支持 Local API 入口前,不得把 Console fresh journey 冒充升级切换验证。
|
||||||
|
|
||||||
## 手工加载与最小 smoke
|
## 手工加载与最小 smoke
|
||||||
|
|
||||||
从 `manifest.json.archive.file` 找到 archive 后加载:
|
从 `manifest.json.archive.file` 找到 archive 后加载:
|
||||||
@@ -162,7 +183,7 @@ docker run --rm --read-only --network none --cap-drop ALL \
|
|||||||
|
|
||||||
## Fresh 试运行边界
|
## Fresh 试运行边界
|
||||||
|
|
||||||
完整 fresh setup、首 Owner ceremony、Owner presentation 安装、Application active、SIGTERM drain、SQLite integrity 和原生 cancellation 必须在 `verification-evidence.json` 指向的同架构 milestone job 中验证。Console 还必须证明首页返回 200、未认证 API 返回 401,并用真实 Owner credential 完成 Task read、fenced start、`succeeded` 终态与 bounded log marker。v7 artifact job 必须从将要上传的目录实际执行 `quickstart.sh`、read-only `upgrade-readiness.sh` 和 reviewed-plan `upgrade-rehearsal.sh`,并完成 graceful stop。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
|
完整 fresh setup、首 Owner ceremony、Owner presentation 安装、Application active、SIGTERM drain、SQLite integrity 和原生 cancellation 必须在 `verification-evidence.json` 指向的同架构 milestone job 中验证。Console 还必须证明首页返回 200、未认证 API 返回 401,并用真实 Owner credential 完成 Task read、fenced start、`succeeded` 终态与 bounded log marker。v8 artifact job 必须从将要上传的目录实际执行 `quickstart.sh`、read-only `upgrade-readiness.sh` 和 isolated `upgrade-cutover-rehearsal.sh`,并完成 graceful stop、rollback-candidate 检查与合成容器清理。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
|
||||||
|
|
||||||
Edge 的验证上限为 Application 128 MiB、0.5 CPU、64 PID;Standalone 为 256 MiB、0.5 CPU、256 PID;operator 为 128 MiB、0.5 CPU、32 PID。这里的数值是试运行门,不是所有 workload 的容量承诺。
|
Edge 的验证上限为 Application 128 MiB、0.5 CPU、64 PID;Standalone 为 256 MiB、0.5 CPU、256 PID;operator 为 128 MiB、0.5 CPU、32 PID。这里的数值是试运行门,不是所有 workload 的容量承诺。
|
||||||
|
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ function validateMilestoneRecord(record, product, sourceRevision, variant) {
|
|||||||
: 'cluster_integration_candidate_not_public_release';
|
: 'cluster_integration_candidate_not_public_release';
|
||||||
const expectedSchema =
|
const expectedSchema =
|
||||||
product === 'local'
|
product === 'local'
|
||||||
? 'qinglong/alpha-local-milestone@v4'
|
? 'qinglong/alpha-local-milestone@v5'
|
||||||
: 'qinglong/alpha-cluster-milestone@v1';
|
: 'qinglong/alpha-cluster-milestone@v1';
|
||||||
if (
|
if (
|
||||||
!exactKeys(record, ['artifactName', 'schema', 'maturity', 'manifest']) ||
|
!exactKeys(record, ['artifactName', 'schema', 'maturity', 'manifest']) ||
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const {
|
|||||||
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
||||||
|
|
||||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
||||||
const SCHEMA = 'qinglong/alpha-local-milestone@v4';
|
const SCHEMA = 'qinglong/alpha-local-milestone@v5';
|
||||||
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
||||||
const FILES = Object.freeze({
|
const FILES = Object.freeze({
|
||||||
readme: 'README.md',
|
readme: 'README.md',
|
||||||
@@ -221,6 +221,7 @@ function bundleRecord(options, architecture) {
|
|||||||
verificationSha256: report.verificationSha256,
|
verificationSha256: report.verificationSha256,
|
||||||
upgradeReadinessSha256: report.upgradeReadinessSha256,
|
upgradeReadinessSha256: report.upgradeReadinessSha256,
|
||||||
upgradeRehearsalSha256: report.upgradeRehearsalSha256,
|
upgradeRehearsalSha256: report.upgradeRehearsalSha256,
|
||||||
|
upgradeCutoverRehearsalSha256: report.upgradeCutoverRehearsalSha256,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +237,7 @@ function validateArtifactRecord(record, architecture, manifest) {
|
|||||||
'verificationSha256',
|
'verificationSha256',
|
||||||
'upgradeReadinessSha256',
|
'upgradeReadinessSha256',
|
||||||
'upgradeRehearsalSha256',
|
'upgradeRehearsalSha256',
|
||||||
|
'upgradeCutoverRehearsalSha256',
|
||||||
]) ||
|
]) ||
|
||||||
record.artifactName !==
|
record.artifactName !==
|
||||||
artifactName(manifest.sourceRevision, architecture, manifest.variant) ||
|
artifactName(manifest.sourceRevision, architecture, manifest.variant) ||
|
||||||
@@ -251,6 +253,7 @@ function validateArtifactRecord(record, architecture, manifest) {
|
|||||||
!SHA256_PATTERN.test(record.verificationSha256 || '') ||
|
!SHA256_PATTERN.test(record.verificationSha256 || '') ||
|
||||||
!SHA256_PATTERN.test(record.upgradeReadinessSha256 || '') ||
|
!SHA256_PATTERN.test(record.upgradeReadinessSha256 || '') ||
|
||||||
!SHA256_PATTERN.test(record.upgradeRehearsalSha256 || '') ||
|
!SHA256_PATTERN.test(record.upgradeRehearsalSha256 || '') ||
|
||||||
|
!SHA256_PATTERN.test(record.upgradeCutoverRehearsalSha256 || '') ||
|
||||||
record.applicationImageId === record.operatorImageId
|
record.applicationImageId === record.operatorImageId
|
||||||
) {
|
) {
|
||||||
fail(`${architecture} milestone artifact record is incompatible`);
|
fail(`${architecture} milestone artifact record is incompatible`);
|
||||||
@@ -319,7 +322,7 @@ function auditLocalAlphaMilestone(options) {
|
|||||||
'artifacts',
|
'artifacts',
|
||||||
'readme',
|
'readme',
|
||||||
]) ||
|
]) ||
|
||||||
manifest.schemaVersion !== 4 ||
|
manifest.schemaVersion !== 5 ||
|
||||||
manifest.schema !== SCHEMA ||
|
manifest.schema !== SCHEMA ||
|
||||||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
||||||
manifest.product !== 'local' ||
|
manifest.product !== 'local' ||
|
||||||
@@ -366,7 +369,9 @@ function auditLocalAlphaMilestone(options) {
|
|||||||
new Set(records.map((record) => record.upgradeReadinessSha256)).size !==
|
new Set(records.map((record) => record.upgradeReadinessSha256)).size !==
|
||||||
ARCHITECTURES.length ||
|
ARCHITECTURES.length ||
|
||||||
new Set(records.map((record) => record.upgradeRehearsalSha256)).size !==
|
new Set(records.map((record) => record.upgradeRehearsalSha256)).size !==
|
||||||
ARCHITECTURES.length
|
ARCHITECTURES.length ||
|
||||||
|
new Set(records.map((record) => record.upgradeCutoverRehearsalSha256))
|
||||||
|
.size !== ARCHITECTURES.length
|
||||||
) {
|
) {
|
||||||
fail('milestone architecture subjects are not distinct');
|
fail('milestone architecture subjects are not distinct');
|
||||||
}
|
}
|
||||||
@@ -393,7 +398,7 @@ function auditLocalAlphaMilestone(options) {
|
|||||||
}
|
}
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
schema: 'qinglong/alpha-local-milestone-audit@v4',
|
schema: 'qinglong/alpha-local-milestone-audit@v5',
|
||||||
sourceRevision: manifest.sourceRevision,
|
sourceRevision: manifest.sourceRevision,
|
||||||
version: manifest.version,
|
version: manifest.version,
|
||||||
variant: manifest.variant,
|
variant: manifest.variant,
|
||||||
@@ -434,7 +439,9 @@ function finalizeLocalAlphaMilestone(options) {
|
|||||||
artifacts.amd64.upgradeReadinessSha256 ===
|
artifacts.amd64.upgradeReadinessSha256 ===
|
||||||
artifacts.arm64.upgradeReadinessSha256 ||
|
artifacts.arm64.upgradeReadinessSha256 ||
|
||||||
artifacts.amd64.upgradeRehearsalSha256 ===
|
artifacts.amd64.upgradeRehearsalSha256 ===
|
||||||
artifacts.arm64.upgradeRehearsalSha256
|
artifacts.arm64.upgradeRehearsalSha256 ||
|
||||||
|
artifacts.amd64.upgradeCutoverRehearsalSha256 ===
|
||||||
|
artifacts.arm64.upgradeCutoverRehearsalSha256
|
||||||
) {
|
) {
|
||||||
fail('milestone architecture subjects must be distinct');
|
fail('milestone architecture subjects must be distinct');
|
||||||
}
|
}
|
||||||
@@ -447,7 +454,7 @@ function finalizeLocalAlphaMilestone(options) {
|
|||||||
path.join(normalized.outputRoot, FILES.readme),
|
path.join(normalized.outputRoot, FILES.readme),
|
||||||
);
|
);
|
||||||
const manifest = {
|
const manifest = {
|
||||||
schemaVersion: 4,
|
schemaVersion: 5,
|
||||||
schema: SCHEMA,
|
schema: SCHEMA,
|
||||||
maturity: 'alpha_candidate_not_public_release',
|
maturity: 'alpha_candidate_not_public_release',
|
||||||
product: 'local',
|
product: 'local',
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ const { auditClusterImageSbom } = require('./ql3-cluster-image-sbom.cjs');
|
|||||||
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs');
|
||||||
|
|
||||||
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
const DEFAULT_ROOT = path.resolve(__dirname, '..');
|
||||||
const SCHEMA = 'qinglong/alpha-local-trial-kit@v7';
|
const SCHEMA = 'qinglong/alpha-local-trial-kit@v8';
|
||||||
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v5';
|
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v6';
|
||||||
const QUICKSTART_TEMPLATE = path.join(
|
const QUICKSTART_TEMPLATE = path.join(
|
||||||
DEFAULT_ROOT,
|
DEFAULT_ROOT,
|
||||||
'scripts/templates/ql3-local-alpha-quickstart.sh',
|
'scripts/templates/ql3-local-alpha-quickstart.sh',
|
||||||
@@ -24,6 +24,10 @@ const UPGRADE_REHEARSAL_TEMPLATE = path.join(
|
|||||||
DEFAULT_ROOT,
|
DEFAULT_ROOT,
|
||||||
'scripts/templates/ql3-local-alpha-upgrade-rehearsal.sh',
|
'scripts/templates/ql3-local-alpha-upgrade-rehearsal.sh',
|
||||||
);
|
);
|
||||||
|
const UPGRADE_CUTOVER_REHEARSAL_TEMPLATE = path.join(
|
||||||
|
DEFAULT_ROOT,
|
||||||
|
'scripts/templates/ql3-local-alpha-upgrade-cutover-rehearsal.sh',
|
||||||
|
);
|
||||||
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
||||||
const VARIANTS = Object.freeze(['headless', 'console']);
|
const VARIANTS = Object.freeze(['headless', 'console']);
|
||||||
const ARCHIVE_MIN_BYTES = 1024;
|
const ARCHIVE_MIN_BYTES = 1024;
|
||||||
@@ -32,6 +36,7 @@ const MAX_README_BYTES = 512 * 1024;
|
|||||||
const MAX_QUICKSTART_BYTES = 256 * 1024;
|
const MAX_QUICKSTART_BYTES = 256 * 1024;
|
||||||
const MAX_UPGRADE_READINESS_BYTES = 256 * 1024;
|
const MAX_UPGRADE_READINESS_BYTES = 256 * 1024;
|
||||||
const MAX_UPGRADE_REHEARSAL_BYTES = 256 * 1024;
|
const MAX_UPGRADE_REHEARSAL_BYTES = 256 * 1024;
|
||||||
|
const MAX_UPGRADE_CUTOVER_REHEARSAL_BYTES = 512 * 1024;
|
||||||
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
||||||
const REVISION_PATTERN = /^[0-9a-f]{40}$/u;
|
const REVISION_PATTERN = /^[0-9a-f]{40}$/u;
|
||||||
const FILES = Object.freeze({
|
const FILES = Object.freeze({
|
||||||
@@ -41,6 +46,7 @@ const FILES = Object.freeze({
|
|||||||
quickstart: 'quickstart.sh',
|
quickstart: 'quickstart.sh',
|
||||||
upgradeReadiness: 'upgrade-readiness.sh',
|
upgradeReadiness: 'upgrade-readiness.sh',
|
||||||
upgradeRehearsal: 'upgrade-rehearsal.sh',
|
upgradeRehearsal: 'upgrade-rehearsal.sh',
|
||||||
|
upgradeCutoverRehearsal: 'upgrade-cutover-rehearsal.sh',
|
||||||
readme: 'README.md',
|
readme: 'README.md',
|
||||||
manifest: 'manifest.json',
|
manifest: 'manifest.json',
|
||||||
checksums: 'SHA256SUMS',
|
checksums: 'SHA256SUMS',
|
||||||
@@ -58,11 +64,13 @@ const VERIFICATION = Object.freeze({
|
|||||||
localApiCancellation: 'passed',
|
localApiCancellation: 'passed',
|
||||||
legacyUpgradeReadiness: 'passed',
|
legacyUpgradeReadiness: 'passed',
|
||||||
legacyUpgradeStage: 'passed',
|
legacyUpgradeStage: 'passed',
|
||||||
|
legacyUpgradeCutover: 'passed',
|
||||||
});
|
});
|
||||||
|
|
||||||
function verificationGates(variant) {
|
function verificationGates(variant) {
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
...VERIFICATION,
|
...VERIFICATION,
|
||||||
|
legacyUpgradeCutover: variant === 'headless' ? 'passed' : 'not_applicable',
|
||||||
consoleLiveJourney: variant === 'console' ? 'passed' : 'not_applicable',
|
consoleLiveJourney: variant === 'console' ? 'passed' : 'not_applicable',
|
||||||
firstAutomationJourney: variant === 'console' ? 'passed' : 'not_applicable',
|
firstAutomationJourney: variant === 'console' ? 'passed' : 'not_applicable',
|
||||||
});
|
});
|
||||||
@@ -513,6 +521,35 @@ function renderUpgradeRehearsal(identity) {
|
|||||||
return rendered;
|
return rendered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderUpgradeCutoverRehearsal(identity) {
|
||||||
|
const template = fs.readFileSync(
|
||||||
|
assertCanonicalFile(
|
||||||
|
UPGRADE_CUTOVER_REHEARSAL_TEMPLATE,
|
||||||
|
MAX_UPGRADE_CUTOVER_REHEARSAL_BYTES,
|
||||||
|
'upgrade cutover rehearsal template',
|
||||||
|
),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const replacements = Object.freeze({
|
||||||
|
'@@APPLICATION_IMAGE@@': identity.images.application.reference,
|
||||||
|
'@@APPLICATION_ID@@': identity.images.application.id,
|
||||||
|
'@@OPERATOR_IMAGE@@': identity.images.operator.reference,
|
||||||
|
'@@OPERATOR_ID@@': identity.images.operator.id,
|
||||||
|
'@@ARCHITECTURE@@': identity.architecture,
|
||||||
|
'@@SOURCE_REVISION@@': identity.sourceRevision,
|
||||||
|
'@@ARCHIVE@@': identity.archive.file,
|
||||||
|
'@@VARIANT@@': identity.variant,
|
||||||
|
});
|
||||||
|
let rendered = template;
|
||||||
|
for (const [token, value] of Object.entries(replacements)) {
|
||||||
|
rendered = rendered.replaceAll(token, value);
|
||||||
|
}
|
||||||
|
if (/@@[A-Z_]+@@/u.test(rendered)) {
|
||||||
|
fail('upgrade cutover rehearsal template contains an unresolved token');
|
||||||
|
}
|
||||||
|
return rendered;
|
||||||
|
}
|
||||||
|
|
||||||
function fileRecord(bundleRoot, name) {
|
function fileRecord(bundleRoot, name) {
|
||||||
const filePath = path.join(bundleRoot, name);
|
const filePath = path.join(bundleRoot, name);
|
||||||
const stat = fs.lstatSync(filePath);
|
const stat = fs.lstatSync(filePath);
|
||||||
@@ -698,8 +735,13 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
renderUpgradeRehearsal(manifestIdentity),
|
renderUpgradeRehearsal(manifestIdentity),
|
||||||
0o700,
|
0o700,
|
||||||
);
|
);
|
||||||
|
writeExclusive(
|
||||||
|
path.join(normalized.outputRoot, FILES.upgradeCutoverRehearsal),
|
||||||
|
renderUpgradeCutoverRehearsal(manifestIdentity),
|
||||||
|
0o700,
|
||||||
|
);
|
||||||
const manifest = {
|
const manifest = {
|
||||||
schemaVersion: 8,
|
schemaVersion: 9,
|
||||||
schema: SCHEMA,
|
schema: SCHEMA,
|
||||||
maturity: 'alpha_candidate_not_public_release',
|
maturity: 'alpha_candidate_not_public_release',
|
||||||
product: 'local',
|
product: 'local',
|
||||||
@@ -722,6 +764,10 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
normalized.outputRoot,
|
normalized.outputRoot,
|
||||||
FILES.upgradeRehearsal,
|
FILES.upgradeRehearsal,
|
||||||
),
|
),
|
||||||
|
upgradeCutoverRehearsal: fileRecord(
|
||||||
|
normalized.outputRoot,
|
||||||
|
FILES.upgradeCutoverRehearsal,
|
||||||
|
),
|
||||||
readme: fileRecord(normalized.outputRoot, FILES.readme),
|
readme: fileRecord(normalized.outputRoot, FILES.readme),
|
||||||
verification: fileRecord(
|
verification: fileRecord(
|
||||||
normalized.outputRoot,
|
normalized.outputRoot,
|
||||||
@@ -740,6 +786,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
FILES.quickstart,
|
FILES.quickstart,
|
||||||
FILES.upgradeReadiness,
|
FILES.upgradeReadiness,
|
||||||
FILES.upgradeRehearsal,
|
FILES.upgradeRehearsal,
|
||||||
|
FILES.upgradeCutoverRehearsal,
|
||||||
FILES.readme,
|
FILES.readme,
|
||||||
FILES.manifest,
|
FILES.manifest,
|
||||||
];
|
];
|
||||||
@@ -811,10 +858,11 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
'quickstart',
|
'quickstart',
|
||||||
'upgradeReadiness',
|
'upgradeReadiness',
|
||||||
'upgradeRehearsal',
|
'upgradeRehearsal',
|
||||||
|
'upgradeCutoverRehearsal',
|
||||||
'readme',
|
'readme',
|
||||||
'verification',
|
'verification',
|
||||||
]) ||
|
]) ||
|
||||||
manifest.schemaVersion !== 8 ||
|
manifest.schemaVersion !== 9 ||
|
||||||
manifest.schema !== SCHEMA ||
|
manifest.schema !== SCHEMA ||
|
||||||
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
manifest.maturity !== 'alpha_candidate_not_public_release' ||
|
||||||
manifest.product !== 'local' ||
|
manifest.product !== 'local' ||
|
||||||
@@ -895,6 +943,26 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
if (actualUpgradeRehearsal !== expectedUpgradeRehearsal) {
|
if (actualUpgradeRehearsal !== expectedUpgradeRehearsal) {
|
||||||
fail('upgrade rehearsal differs from the canonical staging journey');
|
fail('upgrade rehearsal differs from the canonical staging journey');
|
||||||
}
|
}
|
||||||
|
validateFileRecord(
|
||||||
|
manifest.upgradeCutoverRehearsal,
|
||||||
|
FILES.upgradeCutoverRehearsal,
|
||||||
|
bundleRoot,
|
||||||
|
);
|
||||||
|
const expectedUpgradeCutoverRehearsal =
|
||||||
|
renderUpgradeCutoverRehearsal(manifest);
|
||||||
|
const actualUpgradeCutoverRehearsal = fs.readFileSync(
|
||||||
|
assertCanonicalFile(
|
||||||
|
path.join(bundleRoot, FILES.upgradeCutoverRehearsal),
|
||||||
|
MAX_UPGRADE_CUTOVER_REHEARSAL_BYTES,
|
||||||
|
'upgrade cutover rehearsal',
|
||||||
|
),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
if (actualUpgradeCutoverRehearsal !== expectedUpgradeCutoverRehearsal) {
|
||||||
|
fail(
|
||||||
|
'upgrade cutover rehearsal differs from the canonical cutover journey',
|
||||||
|
);
|
||||||
|
}
|
||||||
validateFileRecord(manifest.readme, FILES.readme, bundleRoot);
|
validateFileRecord(manifest.readme, FILES.readme, bundleRoot);
|
||||||
validateOfflineSbom(
|
validateOfflineSbom(
|
||||||
readBoundedJson(
|
readBoundedJson(
|
||||||
@@ -933,6 +1001,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
FILES.quickstart,
|
FILES.quickstart,
|
||||||
FILES.upgradeReadiness,
|
FILES.upgradeReadiness,
|
||||||
FILES.upgradeRehearsal,
|
FILES.upgradeRehearsal,
|
||||||
|
FILES.upgradeCutoverRehearsal,
|
||||||
expectedArchive,
|
expectedArchive,
|
||||||
].sort();
|
].sort();
|
||||||
const actualFiles = fs
|
const actualFiles = fs
|
||||||
@@ -955,6 +1024,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
FILES.quickstart,
|
FILES.quickstart,
|
||||||
FILES.upgradeReadiness,
|
FILES.upgradeReadiness,
|
||||||
FILES.upgradeRehearsal,
|
FILES.upgradeRehearsal,
|
||||||
|
FILES.upgradeCutoverRehearsal,
|
||||||
FILES.readme,
|
FILES.readme,
|
||||||
FILES.manifest,
|
FILES.manifest,
|
||||||
];
|
];
|
||||||
@@ -968,7 +1038,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
}
|
}
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
schema: 'qinglong/alpha-local-trial-kit-audit@v4',
|
schema: 'qinglong/alpha-local-trial-kit-audit@v5',
|
||||||
sourceRevision: manifest.sourceRevision,
|
sourceRevision: manifest.sourceRevision,
|
||||||
version: manifest.version,
|
version: manifest.version,
|
||||||
architecture: manifest.architecture,
|
architecture: manifest.architecture,
|
||||||
@@ -979,6 +1049,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
quickstartSha256: manifest.quickstart.sha256,
|
quickstartSha256: manifest.quickstart.sha256,
|
||||||
upgradeReadinessSha256: manifest.upgradeReadiness.sha256,
|
upgradeReadinessSha256: manifest.upgradeReadiness.sha256,
|
||||||
upgradeRehearsalSha256: manifest.upgradeRehearsal.sha256,
|
upgradeRehearsalSha256: manifest.upgradeRehearsal.sha256,
|
||||||
|
upgradeCutoverRehearsalSha256: manifest.upgradeCutoverRehearsal.sha256,
|
||||||
verificationSha256: manifest.verification.sha256,
|
verificationSha256: manifest.verification.sha256,
|
||||||
workflowRunId: verificationEvidence.workflow.runId,
|
workflowRunId: verificationEvidence.workflow.runId,
|
||||||
workflowRunAttempt: verificationEvidence.workflow.runAttempt,
|
workflowRunAttempt: verificationEvidence.workflow.runAttempt,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const RUNTIME_NODE_IMAGE =
|
|||||||
'node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436';
|
'node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436';
|
||||||
const RUNTIME_OS_PATCH =
|
const RUNTIME_OS_PATCH =
|
||||||
'RUN apk add --no-cache --upgrade \\\n' +
|
'RUN apk add --no-cache --upgrade \\\n' +
|
||||||
|
' docker-cli=29.5.2-r0 \\\n' +
|
||||||
' libcrypto3=3.5.8-r0 \\\n' +
|
' libcrypto3=3.5.8-r0 \\\n' +
|
||||||
' libssl3=3.5.8-r0';
|
' libssl3=3.5.8-r0';
|
||||||
const BUILD_DEPENDENCIES = Object.freeze({
|
const BUILD_DEPENDENCIES = Object.freeze({
|
||||||
@@ -194,6 +195,7 @@ function auditWorkflow(contents, findings) {
|
|||||||
'ql3-local-operator.cdx.json',
|
'ql3-local-operator.cdx.json',
|
||||||
'image-ref: qinglong3-local-operator:ci-${{ matrix.image_arch }}',
|
'image-ref: qinglong3-local-operator:ci-${{ matrix.image_arch }}',
|
||||||
'"${OPERATOR_IMAGE}" --version',
|
'"${OPERATOR_IMAGE}" --version',
|
||||||
|
'--entrypoint /usr/bin/docker \\\n "${OPERATOR_IMAGE}" --version',
|
||||||
'scripts/ql3-local-alpha-trial-kit-live-contract.cjs',
|
'scripts/ql3-local-alpha-trial-kit-live-contract.cjs',
|
||||||
'scripts/ql3-local-alpha-trial-kit-bundle.cjs',
|
'scripts/ql3-local-alpha-trial-kit-bundle.cjs',
|
||||||
'--mode=record-verification',
|
'--mode=record-verification',
|
||||||
@@ -202,6 +204,9 @@ function auditWorkflow(contents, findings) {
|
|||||||
'sh "${BUNDLE_ROOT}/quickstart.sh" \\\n edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"',
|
'sh "${BUNDLE_ROOT}/quickstart.sh" \\\n edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"',
|
||||||
'docker stop --time 30 "${QUICKSTART_CONTAINER}"',
|
'docker stop --time 30 "${QUICKSTART_CONTAINER}"',
|
||||||
'test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"',
|
'test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"',
|
||||||
|
'sh "${BUNDLE_ROOT}/upgrade-cutover-rehearsal.sh"',
|
||||||
|
'"status":"rollback_candidate"',
|
||||||
|
'docker rm "${TARGET_CONTAINER}" "${LEGACY_CONTAINER}"',
|
||||||
'--application-sbom="${APPLICATION_SBOM}"',
|
'--application-sbom="${APPLICATION_SBOM}"',
|
||||||
'--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json"',
|
'--operator-sbom="${RUNNER_TEMP}/ql3-local-operator.cdx.json"',
|
||||||
'--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json"',
|
'--verification-evidence="${RUNNER_TEMP}/ql3-local-alpha-verification-${{ matrix.image_arch }}.json"',
|
||||||
@@ -228,6 +233,7 @@ function auditWorkflow(contents, findings) {
|
|||||||
'--mode=create',
|
'--mode=create',
|
||||||
'--mode=audit',
|
'--mode=audit',
|
||||||
'/quickstart.sh"',
|
'/quickstart.sh"',
|
||||||
|
'/upgrade-cutover-rehearsal.sh"',
|
||||||
'name: Upload the tested native Local Alpha trial kit',
|
'name: Upload the tested native Local Alpha trial kit',
|
||||||
]) {
|
]) {
|
||||||
const index = contents.indexOf(value, cursor + 1);
|
const index = contents.indexOf(value, cursor + 1);
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
APPLICATION_IMAGE='@@APPLICATION_IMAGE@@'
|
||||||
|
APPLICATION_ID='@@APPLICATION_ID@@'
|
||||||
|
OPERATOR_IMAGE='@@OPERATOR_IMAGE@@'
|
||||||
|
OPERATOR_ID='@@OPERATOR_ID@@'
|
||||||
|
ARCHITECTURE='@@ARCHITECTURE@@'
|
||||||
|
SOURCE_REVISION='@@SOURCE_REVISION@@'
|
||||||
|
ARCHIVE='@@ARCHIVE@@'
|
||||||
|
VARIANT='@@VARIANT@@'
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '%s\n' "QingLong Local Alpha cutover rehearsal failed: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
printf '%s\n' \
|
||||||
|
'usage: sh upgrade-cutover-rehearsal.sh edge|standalone /absolute/legacy-data-root /absolute/new/rehearsal-root <reviewed-sqlite-plan-digest> <reviewed-data-directory-plan-digest> [legacy-container-name] [target-container-name]' >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
valid_digest() {
|
||||||
|
[ "${#1}" -eq 64 ] || return 1
|
||||||
|
case "$1" in *[!0-9a-f]*) return 1 ;; *) return 0 ;; esac
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_digest() {
|
||||||
|
result_file=$1
|
||||||
|
field=$2
|
||||||
|
value=$(sed -n "s/^.*\"$field\":\"\([0-9a-f][0-9a-f]*\)\".*$/\1/p" "$result_file")
|
||||||
|
valid_digest "$value" || fail "$field is missing or invalid in $result_file"
|
||||||
|
printf '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
container_name() {
|
||||||
|
case "$1" in ''|[_.-]*|*[!A-Za-z0-9_.-]*) return 1 ;; *) return 0 ;; esac
|
||||||
|
}
|
||||||
|
|
||||||
|
[ "$#" -ge 5 ] && [ "$#" -le 7 ] || usage
|
||||||
|
[ "$VARIANT" = headless ] || fail 'cutover rehearsal is available only in the headless Trial Kit'
|
||||||
|
profile=$1
|
||||||
|
legacy_root=$2
|
||||||
|
rehearsal_root=$3
|
||||||
|
sqlite_plan_digest=$4
|
||||||
|
directory_plan_digest=$5
|
||||||
|
legacy_name=${6:-ql3-alpha-upgrade-legacy}
|
||||||
|
target_name=${7:-ql3-alpha-upgrade-target}
|
||||||
|
|
||||||
|
case "$profile" in
|
||||||
|
edge) memory=128m; pids=64 ;;
|
||||||
|
standalone) memory=256m; pids=256 ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
container_name "$legacy_name" || fail 'legacy container name is invalid'
|
||||||
|
container_name "$target_name" || fail 'target container name is invalid'
|
||||||
|
[ "$legacy_name" != "$target_name" ] || fail 'container names must be distinct'
|
||||||
|
valid_digest "$sqlite_plan_digest" || fail 'reviewed SQLite plan digest is invalid'
|
||||||
|
valid_digest "$directory_plan_digest" || fail 'reviewed data-directory plan digest is invalid'
|
||||||
|
[ "$(uname -s)" = Linux ] || fail 'cutover rehearsal requires a Linux Docker host'
|
||||||
|
for tool in docker sha256sum grep sed stat date; do
|
||||||
|
command -v "$tool" >/dev/null 2>&1 || fail "$tool is required"
|
||||||
|
done
|
||||||
|
[ -S /var/run/docker.sock ] || fail 'canonical Docker socket is unavailable'
|
||||||
|
|
||||||
|
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||||
|
(CDPATH= cd -- "$script_dir" && sha256sum --check SHA256SUMS)
|
||||||
|
docker info >/dev/null 2>&1 || fail 'docker daemon is unavailable'
|
||||||
|
docker load --input "$script_dir/$ARCHIVE" >/dev/null
|
||||||
|
application_identity=$(docker image inspect --format '{{.Id}}|{{.Architecture}}|{{.Config.User}}|{{index .Config.Labels "org.opencontainers.image.revision"}}' "$APPLICATION_IMAGE")
|
||||||
|
operator_identity=$(docker image inspect --format '{{.Id}}|{{.Architecture}}|{{.Config.User}}|{{index .Config.Labels "org.opencontainers.image.revision"}}|{{index .Config.Labels "io.qinglong.lifecycle"}}|{{index .Config.Labels "io.qinglong.network"}}' "$OPERATOR_IMAGE")
|
||||||
|
[ "$application_identity" = "$APPLICATION_ID|$ARCHITECTURE|65532:65532|$SOURCE_REVISION" ] || fail 'application image identity is incompatible'
|
||||||
|
[ "$operator_identity" = "$OPERATOR_ID|$ARCHITECTURE|65532:65532|$SOURCE_REVISION|short-lived|none-by-default" ] || fail 'operator image identity is incompatible'
|
||||||
|
docker run --rm --read-only --network none --cap-drop ALL \
|
||||||
|
--security-opt no-new-privileges --entrypoint /usr/bin/docker \
|
||||||
|
"$OPERATOR_IMAGE" --version >/dev/null
|
||||||
|
|
||||||
|
[ ! -e "$rehearsal_root" ] || fail 'rehearsal root must not already exist'
|
||||||
|
sh "$script_dir/upgrade-rehearsal.sh" \
|
||||||
|
"$profile" "$legacy_root" "$rehearsal_root" \
|
||||||
|
"$sqlite_plan_digest" "$directory_plan_digest"
|
||||||
|
|
||||||
|
legacy_sha256=$(sha256sum "$legacy_root/db/database.sqlite" | sed 's/ .*//')
|
||||||
|
uid=$(id -u)
|
||||||
|
gid=$(id -g)
|
||||||
|
[ "$uid" -eq 0 ] && allow_root_service=true || allow_root_service=false
|
||||||
|
socket_gid=$(stat -c %g /var/run/docker.sock)
|
||||||
|
old_umask=$(umask)
|
||||||
|
umask 077
|
||||||
|
for directory in owner-peppers owner-pepper-backup owner-delivery receipts artifacts plugin-staging plugin-activation service service/cutovers service/cutovers/alpha-upgrade-cutover data-directory/transformation; do
|
||||||
|
[ -d "$rehearsal_root/$directory" ] || mkdir -m 0700 "$rehearsal_root/$directory"
|
||||||
|
done
|
||||||
|
|
||||||
|
run_operator() {
|
||||||
|
command_name=$1
|
||||||
|
command_file=$2
|
||||||
|
result_file=$3
|
||||||
|
docker run --rm --read-only --user "$uid:$gid" --network none \
|
||||||
|
--cap-drop ALL --security-opt no-new-privileges \
|
||||||
|
--memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \
|
||||||
|
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
|
||||||
|
--mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" \
|
||||||
|
--mount "type=bind,src=$rehearsal_root,dst=$rehearsal_root" \
|
||||||
|
"$OPERATOR_IMAGE" "$command_name" run \
|
||||||
|
--command-file "$rehearsal_root/commands/$command_file" \
|
||||||
|
>"$rehearsal_root/results/$result_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_deploy() {
|
||||||
|
subcommand=$1
|
||||||
|
command_file=$2
|
||||||
|
result_file=$3
|
||||||
|
docker run --rm --read-only --user "$uid:$gid" \
|
||||||
|
--group-add "$socket_gid" --network none \
|
||||||
|
--cap-drop ALL --security-opt no-new-privileges \
|
||||||
|
--memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \
|
||||||
|
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
|
||||||
|
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
|
||||||
|
--mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" \
|
||||||
|
--mount "type=bind,src=$rehearsal_root,dst=$rehearsal_root" \
|
||||||
|
"$OPERATOR_IMAGE" deploy "$subcommand" \
|
||||||
|
--command-file "$rehearsal_root/commands/$command_file" \
|
||||||
|
>"$rehearsal_root/results/$result_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_deployment_offline() {
|
||||||
|
subcommand=$1
|
||||||
|
command_file=$2
|
||||||
|
result_file=$3
|
||||||
|
docker run --rm --read-only --user "$uid:$gid" --network none \
|
||||||
|
--cap-drop ALL --security-opt no-new-privileges \
|
||||||
|
--memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \
|
||||||
|
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
|
||||||
|
--mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" \
|
||||||
|
--mount "type=bind,src=$rehearsal_root,dst=$rehearsal_root" \
|
||||||
|
"$OPERATOR_IMAGE" deploy "$subcommand" \
|
||||||
|
--command-file "$rehearsal_root/commands/$command_file" \
|
||||||
|
>"$rehearsal_root/results/$result_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
cat >"$rehearsal_root/commands/setup.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"local.setup.prepare","options":{"deploymentRoot":"$rehearsal_root","databasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","profile":"$profile","ownerPepperKeyringDirectory":"$rehearsal_root/owner-peppers","ownerPepperBackupDirectory":"$rehearsal_root/owner-pepper-backup","ownerPepperKeyId":"owner-v1","localSecretKeyringPath":"$rehearsal_root/local-secret-keyring.json","busyTimeoutMs":100},"request":{"registerMutationId":"019f8680-143d-4000-8000-000000000111","activateMutationId":"019f8680-143d-4000-8000-000000000112","registeredAtMs":1785254400100,"activatedAtMs":1785254400101}}
|
||||||
|
EOF
|
||||||
|
cat >"$rehearsal_root/commands/owner-provision.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"owner.identity.provision","options":{"deploymentRoot":"$rehearsal_root","databasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","pepperPath":"$rehearsal_root/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"$rehearsal_root/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"mutationId":"019f8680-143d-4000-8000-000000000121","requestId":"alpha-upgrade-owner-provision"}}
|
||||||
|
EOF
|
||||||
|
cat >"$rehearsal_root/commands/owner-challenge.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"owner.challenge.issue","options":{"deploymentRoot":"$rehearsal_root","databasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","pepperPath":"$rehearsal_root/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"$rehearsal_root/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"projectId":"default","mutationId":"019f8680-143d-4000-8000-000000000122","requestId":"alpha-upgrade-owner-challenge"}}
|
||||||
|
EOF
|
||||||
|
cat >"$rehearsal_root/commands/owner-claim.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"owner.claim.from-deliveries","options":{"deploymentRoot":"$rehearsal_root","databasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","pepperPath":"$rehearsal_root/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"$rehearsal_root/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"projectId":"default","mutationId":"019f8680-143d-4000-8000-000000000123","requestId":"alpha-upgrade-owner-claim","credentialMutationId":"019f8680-143d-4000-8000-000000000121","challengeMutationId":"019f8680-143d-4000-8000-000000000122"}}
|
||||||
|
EOF
|
||||||
|
cat >"$rehearsal_root/commands/owner-credential-install.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"owner.credential-presentation.install-from-delivery","options":{"deploymentRoot":"$rehearsal_root","databasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","pepperPath":"$rehearsal_root/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"$rehearsal_root/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"credentialMutationId":"019f8680-143d-4000-8000-000000000121","destinationFilePath":"$rehearsal_root/owner-credential.json"}}
|
||||||
|
EOF
|
||||||
|
chmod 0600 "$rehearsal_root/commands"/*.json
|
||||||
|
run_operator setup setup.json setup.result.json
|
||||||
|
run_operator owner owner-provision.json owner-provision.result.json
|
||||||
|
run_operator owner owner-challenge.json owner-challenge.result.json
|
||||||
|
run_operator owner owner-claim.json owner-claim.result.json
|
||||||
|
run_operator owner owner-credential-install.json owner-credential-install.result.json
|
||||||
|
grep -q '"role":"owner"' "$rehearsal_root/results/owner-claim.result.json" || fail 'Owner claim did not establish owner authority'
|
||||||
|
|
||||||
|
directory_manifest_digest=$(extract_digest "$rehearsal_root/stage-summary.json" manifestDigest)
|
||||||
|
activation_digest=$(extract_digest "$rehearsal_root/stage-summary.json" activationDigest)
|
||||||
|
cat >"$rehearsal_root/commands/data-directory-transform.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"local-data-directory.adoption.transform","options":{"deploymentRoot":"$rehearsal_root","dataRoot":"$legacy_root","stagingRoot":"$rehearsal_root/data-directory/staged","transformationRoot":"$rehearsal_root/data-directory/transformation","projectId":"default","profile":"$profile","expectedManifestDigest":"$directory_manifest_digest","sqlite":{"sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"}}}
|
||||||
|
EOF
|
||||||
|
chmod 0600 "$rehearsal_root/commands/data-directory-transform.json"
|
||||||
|
run_operator adoption data-directory-transform.json data-directory-transform.result.json
|
||||||
|
grep -q '"assessment":"ready"' "$rehearsal_root/results/data-directory-transform.result.json" || fail 'data-directory transformation is not ready'
|
||||||
|
transformation_digest=$(extract_digest "$rehearsal_root/results/data-directory-transform.result.json" transformationDigest)
|
||||||
|
sed "s/\"operation\":\"local-data-directory.adoption.transform\"/\"operation\":\"local-data-directory.adoption.transform.verify\"/;s/\"sqlite\":{/\"expectedTransformationDigest\":\"$transformation_digest\",\"sqlite\":{/" \
|
||||||
|
"$rehearsal_root/commands/data-directory-transform.json" \
|
||||||
|
>"$rehearsal_root/commands/data-directory-transform-verify.json"
|
||||||
|
chmod 0600 "$rehearsal_root/commands/data-directory-transform-verify.json"
|
||||||
|
run_operator adoption data-directory-transform-verify.json data-directory-transform-verify.result.json
|
||||||
|
grep -q '"status":"verified"' "$rehearsal_root/results/data-directory-transform-verify.result.json" || fail 'data-directory transformation verification failed'
|
||||||
|
|
||||||
|
cleanup_required=1
|
||||||
|
cleanup() {
|
||||||
|
if [ "$cleanup_required" -eq 1 ]; then
|
||||||
|
docker rm --force "$target_name" "$legacy_name" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
trap 'exit 130' HUP INT TERM
|
||||||
|
legacy_id=$(docker run --detach --name "$legacy_name" --restart no \
|
||||||
|
--read-only --user "$uid:$gid" --network none --cap-drop ALL \
|
||||||
|
--security-opt no-new-privileges --memory 64m --memory-swap 64m \
|
||||||
|
--pids-limit 16 --tmpfs /tmp:rw,nosuid,nodev,noexec,size=4m \
|
||||||
|
--mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" \
|
||||||
|
--entrypoint /usr/local/bin/node "$OPERATOR_IMAGE" \
|
||||||
|
-e 'setInterval(() => {}, 60000)')
|
||||||
|
[ "${#legacy_id}" -eq 64 ] || fail 'legacy container ID is invalid'
|
||||||
|
case "$legacy_id" in *[!0-9a-f]*) fail 'legacy container ID is invalid' ;; esac
|
||||||
|
|
||||||
|
now_ms=$(($(date +%s) * 1000))
|
||||||
|
cat >"$rehearsal_root/commands/legacy-stop.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"local.deployment.cutover.legacy-stop","options":{"deploymentRoot":"$rehearsal_root","dockerExecutable":"/usr/bin/docker","dockerSocketPath":"/var/run/docker.sock","allowRootService":$allow_root_service},"request":{"cutoverId":"alpha-upgrade-cutover","profile":"$profile","instanceId":"alpha-upgrade","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","legacySourcePath":"$legacy_root/db/database.sqlite","targetDatabasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","expectedLegacyDatabasePath":"$legacy_root/db/database.sqlite","expectedActivationDigest":"$activation_digest","expectedLegacyContainerId":"$legacy_id","requestedAtMs":$now_ms}}
|
||||||
|
EOF
|
||||||
|
chmod 0600 "$rehearsal_root/commands/legacy-stop.json"
|
||||||
|
run_deploy cutover-legacy-stop legacy-stop.json legacy-stop.result.json
|
||||||
|
grep -q '"state":"legacy_stopped"' "$rehearsal_root/results/legacy-stop.result.json" || fail 'legacy container did not stop cleanly'
|
||||||
|
commitment_digest=$(extract_digest "$rehearsal_root/results/legacy-stop.result.json" commitmentDigest)
|
||||||
|
|
||||||
|
cat >"$rehearsal_root/commands/data-directory-apply.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"local-data-directory.adoption.apply","options":{"deploymentRoot":"$rehearsal_root","dataRoot":"$legacy_root","stagingRoot":"$rehearsal_root/data-directory/staged","transformationRoot":"$rehearsal_root/data-directory/transformation","projectId":"default","profile":"$profile","expectedManifestDigest":"$directory_manifest_digest","expectedTransformationDigest":"$transformation_digest","ownerPepperKeyringDirectory":"$rehearsal_root/owner-peppers","credentialFilePath":"$rehearsal_root/owner-credential.json","secretKeyringPath":"$rehearsal_root/local-secret-keyring.json","mutationId":"019f8680-143d-4000-8000-000000000131","failureAuditEventId":"019f8680-143d-4000-8000-000000000132","requestId":"alpha-upgrade-data-apply","sqlite":{"sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"}}}
|
||||||
|
EOF
|
||||||
|
chmod 0600 "$rehearsal_root/commands/data-directory-apply.json"
|
||||||
|
run_operator adoption data-directory-apply.json data-directory-apply.result.json
|
||||||
|
grep -q '"status":"committed"' "$rehearsal_root/results/data-directory-apply.result.json" || fail 'authenticated data application did not commit'
|
||||||
|
commit_digest=$(extract_digest "$rehearsal_root/results/data-directory-apply.result.json" commitDigest)
|
||||||
|
receipt_digest=$(extract_digest "$rehearsal_root/results/data-directory-apply.result.json" receiptDigest)
|
||||||
|
sed "s/\"operation\":\"local-data-directory.adoption.apply\"/\"operation\":\"local-data-directory.adoption.apply.verify\"/;s/\"sqlite\":{/\"expectedReceiptDigest\":\"$receipt_digest\",\"sqlite\":{/" \
|
||||||
|
"$rehearsal_root/commands/data-directory-apply.json" \
|
||||||
|
>"$rehearsal_root/commands/data-directory-apply-verify.json"
|
||||||
|
chmod 0600 "$rehearsal_root/commands/data-directory-apply-verify.json"
|
||||||
|
run_operator adoption data-directory-apply-verify.json data-directory-apply-verify.result.json
|
||||||
|
grep -q '"status":"verified"' "$rehearsal_root/results/data-directory-apply-verify.result.json" || fail 'authenticated data application verification failed'
|
||||||
|
|
||||||
|
prepared_ms=$((now_ms + 1))
|
||||||
|
cat >"$rehearsal_root/commands/adopted-prepare.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"local.deployment.adopted.prepare","options":{"deploymentRoot":"$rehearsal_root","profile":"$profile","instanceId":"alpha-upgrade","busyTimeoutMs":100,"service":{"kind":"docker-target","targetImage":{"authority":"local-image-id","reference":"$APPLICATION_IMAGE","imageId":"$APPLICATION_ID"},"allowRootService":$allow_root_service}},"request":{"bundleId":"019f8680-143d-4000-8000-000000000141","preparedAtMs":$prepared_ms,"cutoverId":"alpha-upgrade-cutover","storage":{"sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"},"cutover":{"commitmentPath":"$rehearsal_root/service/cutovers/alpha-upgrade-cutover/0002-legacy-stopped.json","expectedCommitmentDigest":"$commitment_digest"},"legacyDataApplication":{"commitPath":"$rehearsal_root/data-directory/transformation/commit.json","expectedCommitDigest":"$commit_digest","expectedReceiptDigest":"$receipt_digest"}}}
|
||||||
|
EOF
|
||||||
|
chmod 0600 "$rehearsal_root/commands/adopted-prepare.json"
|
||||||
|
run_deployment_offline adopted-prepare adopted-prepare.json adopted-prepare.result.json
|
||||||
|
grep -q '"status":"prepared"' "$rehearsal_root/results/adopted-prepare.result.json" || fail 'adopted bundle was not prepared'
|
||||||
|
sed 's/"operation":"local.deployment.adopted.prepare"/"operation":"local.deployment.adopted.verify"/' \
|
||||||
|
"$rehearsal_root/commands/adopted-prepare.json" \
|
||||||
|
>"$rehearsal_root/commands/adopted-verify.json"
|
||||||
|
chmod 0600 "$rehearsal_root/commands/adopted-verify.json"
|
||||||
|
run_deployment_offline adopted-verify adopted-verify.json adopted-verify.result.json
|
||||||
|
grep -q '"status":"verified"' "$rehearsal_root/results/adopted-verify.result.json" || fail 'adopted bundle verification failed'
|
||||||
|
bundle_digest=$(extract_digest "$rehearsal_root/results/adopted-verify.result.json" bundleDigest)
|
||||||
|
grep -Fq "\"reference\": \"$APPLICATION_IMAGE\"" "$rehearsal_root/service/docker-target.json" || fail 'target descriptor image reference drifted'
|
||||||
|
grep -Fq "\"imageId\": \"$APPLICATION_ID\"" "$rehearsal_root/service/docker-target.json" || fail 'target descriptor image ID drifted'
|
||||||
|
|
||||||
|
target_id=$(docker create --name "$target_name" --restart no \
|
||||||
|
--read-only --user "$uid:$gid" --network none --cap-drop ALL \
|
||||||
|
--security-opt no-new-privileges --memory "$memory" --memory-swap "$memory" \
|
||||||
|
--cpus 0.5 --pids-limit "$pids" \
|
||||||
|
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=16m \
|
||||||
|
--mount "type=bind,src=$rehearsal_root,dst=$rehearsal_root" \
|
||||||
|
--mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" \
|
||||||
|
"$APPLICATION_IMAGE" --config "$rehearsal_root/local-application.json")
|
||||||
|
[ "${#target_id}" -eq 64 ] || fail 'target container ID is invalid'
|
||||||
|
case "$target_id" in *[!0-9a-f]*) fail 'target container ID is invalid' ;; esac
|
||||||
|
|
||||||
|
start_ms=$((prepared_ms + 1))
|
||||||
|
cat >"$rehearsal_root/commands/target-start.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"local.deployment.cutover.target-start","options":{"deploymentRoot":"$rehearsal_root","dockerExecutable":"/usr/bin/docker","dockerSocketPath":"/var/run/docker.sock","allowRootService":$allow_root_service},"request":{"cutoverId":"alpha-upgrade-cutover","profile":"$profile","instanceId":"alpha-upgrade","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","legacySourcePath":"$legacy_root/db/database.sqlite","targetDatabasePath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","expectedLegacyDatabasePath":"$legacy_root/db/database.sqlite","expectedActivationDigest":"$activation_digest","expectedLegacyCommitmentDigest":"$commitment_digest","expectedLegacyContainerId":"$legacy_id","expectedTargetContainerId":"$target_id","targetImage":{"authority":"local-image-id","reference":"$APPLICATION_IMAGE","imageId":"$APPLICATION_ID"},"applicationConfigPath":"$rehearsal_root/local-application.json","expectedTargetApplicationConfigPath":"$rehearsal_root/local-application.json","expectedTargetCommitmentPath":"$rehearsal_root/service/cutovers/alpha-upgrade-cutover/0002-legacy-stopped.json","generation":1,"requestedAtMs":$start_ms}}
|
||||||
|
EOF
|
||||||
|
chmod 0600 "$rehearsal_root/commands/target-start.json"
|
||||||
|
run_deploy cutover-target-start target-start.json target-start.result.json
|
||||||
|
grep -q '"state":"target_active"' "$rehearsal_root/results/target-start.result.json" || fail 'target did not become active'
|
||||||
|
stop_ms=$((start_ms + 1))
|
||||||
|
sed "s/\"operation\":\"local.deployment.cutover.target-start\"/\"operation\":\"local.deployment.cutover.target-stop\"/;s/\"requestedAtMs\":$start_ms/\"requestedAtMs\":$stop_ms/" \
|
||||||
|
"$rehearsal_root/commands/target-start.json" \
|
||||||
|
>"$rehearsal_root/commands/target-stop.json"
|
||||||
|
chmod 0600 "$rehearsal_root/commands/target-stop.json"
|
||||||
|
run_deploy cutover-target-stop target-stop.json target-stop.result.json
|
||||||
|
grep -q '"reconciliation":"rollback_candidate"' "$rehearsal_root/results/target-stop.result.json" || fail 'target stop did not produce a clean rollback candidate'
|
||||||
|
[ "$(sha256sum "$legacy_root/db/database.sqlite" | sed 's/ .*//')" = "$legacy_sha256" ] || fail 'legacy database changed during rehearsal'
|
||||||
|
|
||||||
|
cat >"$rehearsal_root/cutover-summary.json" <<EOF
|
||||||
|
{"schemaVersion":1,"schema":"qinglong/local-alpha-upgrade-cutover-summary@v1","status":"rollback_candidate","profile":"$profile","sourceRevision":"$SOURCE_REVISION","architecture":"$ARCHITECTURE","reviewedPlans":{"sqlite":"$sqlite_plan_digest","dataDirectory":"$directory_plan_digest"},"activationDigest":"$activation_digest","transformationDigest":"$transformation_digest","application":{"commitDigest":"$commit_digest","receiptDigest":"$receipt_digest"},"cutover":{"commitmentDigest":"$commitment_digest","bundleDigest":"$bundle_digest","legacyContainerId":"$legacy_id","targetContainerId":"$target_id"},"legacySource":"unchanged","target":"stopped","rollback":"candidate_not_executed"}
|
||||||
|
EOF
|
||||||
|
chmod 0600 "$rehearsal_root/cutover-summary.json"
|
||||||
|
umask "$old_umask"
|
||||||
|
cleanup_required=0
|
||||||
|
trap - EXIT HUP INT TERM
|
||||||
|
|
||||||
|
printf '%s\n' \
|
||||||
|
"QingLong 3.0 isolated cutover rehearsal completed ($profile, $ARCHITECTURE)." \
|
||||||
|
"Summary: $rehearsal_root/cutover-summary.json" \
|
||||||
|
"Stopped legacy container: $legacy_name ($legacy_id)" \
|
||||||
|
"Stopped target container: $target_name ($target_id)" \
|
||||||
|
"Remove after review: docker rm $target_name $legacy_name" \
|
||||||
|
'The original legacy SQLite remained unchanged. Production cutover and Legacy restart were not authorized.'
|
||||||
@@ -99,10 +99,10 @@ for directory in commands results sqlite data-directory; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
cat >"$rehearsal_root/commands/sqlite-stage.json" <<EOF
|
cat >"$rehearsal_root/commands/sqlite-stage.json" <<EOF
|
||||||
{"schemaVersion":1,"operation":"local-sqlite.adoption.stage","options":{"deploymentRoot":"/var/lib/qinglong3","profile":"$profile","sourcePath":"/var/lib/qinglong2/db/database.sqlite","targetPath":"/var/lib/qinglong3/sqlite/qinglong3.sqlite","recoveryPath":"/var/lib/qinglong3/sqlite/database.pre-ql3.sqlite","manifestPath":"/var/lib/qinglong3/sqlite/qinglong3-adoption.json","expectedPlanDigest":"$sqlite_plan_digest"}}
|
{"schemaVersion":1,"operation":"local-sqlite.adoption.stage","options":{"deploymentRoot":"$rehearsal_root","profile":"$profile","sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","expectedPlanDigest":"$sqlite_plan_digest"}}
|
||||||
EOF
|
EOF
|
||||||
cat >"$rehearsal_root/commands/sqlite-verify.json" <<EOF
|
cat >"$rehearsal_root/commands/sqlite-verify.json" <<EOF
|
||||||
{"schemaVersion":1,"operation":"local-sqlite.adoption.verify","options":{"deploymentRoot":"/var/lib/qinglong3","profile":"$profile","targetPath":"/var/lib/qinglong3/sqlite/qinglong3.sqlite","recoveryPath":"/var/lib/qinglong3/sqlite/database.pre-ql3.sqlite","manifestPath":"/var/lib/qinglong3/sqlite/qinglong3-adoption.json"}}
|
{"schemaVersion":1,"operation":"local-sqlite.adoption.verify","options":{"deploymentRoot":"$rehearsal_root","profile":"$profile","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json"}}
|
||||||
EOF
|
EOF
|
||||||
chmod 0600 "$rehearsal_root/commands/sqlite-stage.json" "$rehearsal_root/commands/sqlite-verify.json"
|
chmod 0600 "$rehearsal_root/commands/sqlite-stage.json" "$rehearsal_root/commands/sqlite-verify.json"
|
||||||
|
|
||||||
@@ -115,10 +115,10 @@ run_adoption() {
|
|||||||
--cap-drop ALL --security-opt no-new-privileges \
|
--cap-drop ALL --security-opt no-new-privileges \
|
||||||
--memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \
|
--memory 128m --memory-swap 128m --cpus 0.5 --pids-limit 32 \
|
||||||
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
|
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=8m \
|
||||||
--mount "type=bind,src=$legacy_root,dst=/var/lib/qinglong2,readonly" \
|
--mount "type=bind,src=$legacy_root,dst=$legacy_root,readonly" \
|
||||||
--mount "type=bind,src=$rehearsal_root,dst=/var/lib/qinglong3" \
|
--mount "type=bind,src=$rehearsal_root,dst=$rehearsal_root" \
|
||||||
"$OPERATOR_IMAGE" adoption run \
|
"$OPERATOR_IMAGE" adoption run \
|
||||||
--command-file "/var/lib/qinglong3/commands/$command_file" \
|
--command-file "$rehearsal_root/commands/$command_file" \
|
||||||
>"$rehearsal_root/results/$result_file"
|
>"$rehearsal_root/results/$result_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ grep -q '"status":"verified"' "$rehearsal_root/results/sqlite-verify.result.json
|
|||||||
sqlite_manifest_digest=$(extract_digest "$rehearsal_root/results/sqlite-verify.result.json" manifestDigest)
|
sqlite_manifest_digest=$(extract_digest "$rehearsal_root/results/sqlite-verify.result.json" manifestDigest)
|
||||||
|
|
||||||
cat >"$rehearsal_root/commands/sqlite-activation.json" <<EOF
|
cat >"$rehearsal_root/commands/sqlite-activation.json" <<EOF
|
||||||
{"schemaVersion":1,"operation":"local-sqlite.activation.prepare","options":{"deploymentRoot":"/var/lib/qinglong3","profile":"$profile","sourcePath":"/var/lib/qinglong2/db/database.sqlite","targetPath":"/var/lib/qinglong3/sqlite/qinglong3.sqlite","recoveryPath":"/var/lib/qinglong3/sqlite/database.pre-ql3.sqlite","manifestPath":"/var/lib/qinglong3/sqlite/qinglong3-adoption.json","activationPath":"/var/lib/qinglong3/sqlite/qinglong3-activation.json","expectedManifestDigest":"$sqlite_manifest_digest"}}
|
{"schemaVersion":1,"operation":"local-sqlite.activation.prepare","options":{"deploymentRoot":"$rehearsal_root","profile":"$profile","sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","expectedManifestDigest":"$sqlite_manifest_digest"}}
|
||||||
EOF
|
EOF
|
||||||
chmod 0600 "$rehearsal_root/commands/sqlite-activation.json"
|
chmod 0600 "$rehearsal_root/commands/sqlite-activation.json"
|
||||||
run_adoption sqlite-activation.json sqlite-activation.result.json
|
run_adoption sqlite-activation.json sqlite-activation.result.json
|
||||||
@@ -137,7 +137,7 @@ grep -q '"status":"prepared"' "$rehearsal_root/results/sqlite-activation.result.
|
|||||||
activation_digest=$(extract_digest "$rehearsal_root/results/sqlite-activation.result.json" activationDigest)
|
activation_digest=$(extract_digest "$rehearsal_root/results/sqlite-activation.result.json" activationDigest)
|
||||||
|
|
||||||
cat >"$rehearsal_root/commands/data-directory-stage.json" <<EOF
|
cat >"$rehearsal_root/commands/data-directory-stage.json" <<EOF
|
||||||
{"schemaVersion":1,"operation":"local-data-directory.adoption.stage","options":{"deploymentRoot":"/var/lib/qinglong3","dataRoot":"/var/lib/qinglong2","stagingRoot":"/var/lib/qinglong3/data-directory/staged","profile":"$profile","expectedPlanDigest":"$directory_plan_digest","sqlite":{"sourcePath":"/var/lib/qinglong2/db/database.sqlite","targetPath":"/var/lib/qinglong3/sqlite/qinglong3.sqlite","recoveryPath":"/var/lib/qinglong3/sqlite/database.pre-ql3.sqlite","manifestPath":"/var/lib/qinglong3/sqlite/qinglong3-adoption.json","activationPath":"/var/lib/qinglong3/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"}}}
|
{"schemaVersion":1,"operation":"local-data-directory.adoption.stage","options":{"deploymentRoot":"$rehearsal_root","dataRoot":"$legacy_root","stagingRoot":"$rehearsal_root/data-directory/staged","profile":"$profile","expectedPlanDigest":"$directory_plan_digest","sqlite":{"sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"}}}
|
||||||
EOF
|
EOF
|
||||||
chmod 0600 "$rehearsal_root/commands/data-directory-stage.json"
|
chmod 0600 "$rehearsal_root/commands/data-directory-stage.json"
|
||||||
run_adoption data-directory-stage.json data-directory-stage.result.json
|
run_adoption data-directory-stage.json data-directory-stage.result.json
|
||||||
@@ -145,7 +145,7 @@ grep -q '"status":"staged"' "$rehearsal_root/results/data-directory-stage.result
|
|||||||
directory_manifest_digest=$(extract_digest "$rehearsal_root/results/data-directory-stage.result.json" manifestDigest)
|
directory_manifest_digest=$(extract_digest "$rehearsal_root/results/data-directory-stage.result.json" manifestDigest)
|
||||||
|
|
||||||
cat >"$rehearsal_root/commands/data-directory-verify.json" <<EOF
|
cat >"$rehearsal_root/commands/data-directory-verify.json" <<EOF
|
||||||
{"schemaVersion":1,"operation":"local-data-directory.adoption.verify","options":{"deploymentRoot":"/var/lib/qinglong3","dataRoot":"/var/lib/qinglong2","stagingRoot":"/var/lib/qinglong3/data-directory/staged","profile":"$profile","expectedManifestDigest":"$directory_manifest_digest","sqlite":{"sourcePath":"/var/lib/qinglong2/db/database.sqlite","targetPath":"/var/lib/qinglong3/sqlite/qinglong3.sqlite","recoveryPath":"/var/lib/qinglong3/sqlite/database.pre-ql3.sqlite","manifestPath":"/var/lib/qinglong3/sqlite/qinglong3-adoption.json","activationPath":"/var/lib/qinglong3/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"}}}
|
{"schemaVersion":1,"operation":"local-data-directory.adoption.verify","options":{"deploymentRoot":"$rehearsal_root","dataRoot":"$legacy_root","stagingRoot":"$rehearsal_root/data-directory/staged","profile":"$profile","expectedManifestDigest":"$directory_manifest_digest","sqlite":{"sourcePath":"$legacy_root/db/database.sqlite","targetPath":"$rehearsal_root/sqlite/qinglong3.sqlite","recoveryPath":"$rehearsal_root/sqlite/database.pre-ql3.sqlite","manifestPath":"$rehearsal_root/sqlite/qinglong3-adoption.json","activationPath":"$rehearsal_root/sqlite/qinglong3-activation.json","expectedActivationDigest":"$activation_digest"}}}
|
||||||
EOF
|
EOF
|
||||||
chmod 0600 "$rehearsal_root/commands/data-directory-verify.json"
|
chmod 0600 "$rehearsal_root/commands/data-directory-verify.json"
|
||||||
run_adoption data-directory-verify.json data-directory-verify.result.json
|
run_adoption data-directory-verify.json data-directory-verify.result.json
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ function writeMilestone(directory, manifest) {
|
|||||||
|
|
||||||
function localManifest(attempt = runAttempt, variant = 'headless') {
|
function localManifest(attempt = runAttempt, variant = 'headless') {
|
||||||
return {
|
return {
|
||||||
schemaVersion: 4,
|
schemaVersion: 5,
|
||||||
schema: 'qinglong/alpha-local-milestone@v4',
|
schema: 'qinglong/alpha-local-milestone@v5',
|
||||||
maturity: 'alpha_candidate_not_public_release',
|
maturity: 'alpha_candidate_not_public_release',
|
||||||
product: 'local',
|
product: 'local',
|
||||||
variant,
|
variant,
|
||||||
@@ -103,6 +103,7 @@ function localManifest(attempt = runAttempt, variant = 'headless') {
|
|||||||
verificationSha256: digest('5'),
|
verificationSha256: digest('5'),
|
||||||
upgradeReadinessSha256: digest('b'),
|
upgradeReadinessSha256: digest('b'),
|
||||||
upgradeRehearsalSha256: digest('d'),
|
upgradeRehearsalSha256: digest('d'),
|
||||||
|
upgradeCutoverRehearsalSha256: digest('f'),
|
||||||
},
|
},
|
||||||
arm64: {
|
arm64: {
|
||||||
artifactName: `ql3-alpha-${revision}-local-${variant}-arm64`,
|
artifactName: `ql3-alpha-${revision}-local-${variant}-arm64`,
|
||||||
@@ -118,6 +119,7 @@ function localManifest(attempt = runAttempt, variant = 'headless') {
|
|||||||
verificationSha256: digest('a'),
|
verificationSha256: digest('a'),
|
||||||
upgradeReadinessSha256: digest('c'),
|
upgradeReadinessSha256: digest('c'),
|
||||||
upgradeRehearsalSha256: digest('e'),
|
upgradeRehearsalSha256: digest('e'),
|
||||||
|
upgradeCutoverRehearsalSha256: digest('0'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
readme: null,
|
readme: null,
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ function finalizeOptions(paths) {
|
|||||||
test('finalizes two exact native trial kits into one closed milestone index', (t) => {
|
test('finalizes two exact native trial kits into one closed milestone index', (t) => {
|
||||||
const paths = fixture(t);
|
const paths = fixture(t);
|
||||||
const manifest = finalizeLocalAlphaMilestone(finalizeOptions(paths));
|
const manifest = finalizeLocalAlphaMilestone(finalizeOptions(paths));
|
||||||
assert.equal(manifest.schema, 'qinglong/alpha-local-milestone@v4');
|
assert.equal(manifest.schema, 'qinglong/alpha-local-milestone@v5');
|
||||||
assert.match(
|
assert.match(
|
||||||
manifest.artifacts.amd64.upgradeReadinessSha256,
|
manifest.artifacts.amd64.upgradeReadinessSha256,
|
||||||
/^sha256:[0-9a-f]{64}$/,
|
/^sha256:[0-9a-f]{64}$/,
|
||||||
@@ -215,6 +215,10 @@ test('finalizes two exact native trial kits into one closed milestone index', (t
|
|||||||
manifest.artifacts.amd64.upgradeRehearsalSha256,
|
manifest.artifacts.amd64.upgradeRehearsalSha256,
|
||||||
/^sha256:[0-9a-f]{64}$/,
|
/^sha256:[0-9a-f]{64}$/,
|
||||||
);
|
);
|
||||||
|
assert.match(
|
||||||
|
manifest.artifacts.amd64.upgradeCutoverRehearsalSha256,
|
||||||
|
/^sha256:[0-9a-f]{64}$/,
|
||||||
|
);
|
||||||
assert.equal(manifest.variant, 'headless');
|
assert.equal(manifest.variant, 'headless');
|
||||||
assert.equal(manifest.sourceRevision, revision);
|
assert.equal(manifest.sourceRevision, revision);
|
||||||
assert.deepEqual(Object.keys(manifest.artifacts), ['amd64', 'arm64']);
|
assert.deepEqual(Object.keys(manifest.artifacts), ['amd64', 'arm64']);
|
||||||
@@ -243,6 +247,7 @@ test('finalizes two exact native trial kits into one closed milestone index', (t
|
|||||||
assert.equal(report.workflowRunId, runId);
|
assert.equal(report.workflowRunId, runId);
|
||||||
assert.equal(report.workflowRunAttempt, runAttempt);
|
assert.equal(report.workflowRunAttempt, runAttempt);
|
||||||
assert.equal(report.variant, 'headless');
|
assert.equal(report.variant, 'headless');
|
||||||
|
assert.equal(report.schema, 'qinglong/alpha-local-milestone-audit@v5');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('finalizes Console trial kits as a separately named milestone', (t) => {
|
test('finalizes Console trial kits as a separately named milestone', (t) => {
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ function adapters(overrides = {}, variant = 'headless') {
|
|||||||
test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
||||||
const paths = fixture(t);
|
const paths = fixture(t);
|
||||||
const manifest = createLocalAlphaTrialKit(createOptions(paths), adapters());
|
const manifest = createLocalAlphaTrialKit(createOptions(paths), adapters());
|
||||||
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v7');
|
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v8');
|
||||||
assert.equal(manifest.variant, 'headless');
|
assert.equal(manifest.variant, 'headless');
|
||||||
assert.equal(manifest.sourceRevision, revision);
|
assert.equal(manifest.sourceRevision, revision);
|
||||||
assert.equal(manifest.architecture, 'arm64');
|
assert.equal(manifest.architecture, 'arm64');
|
||||||
@@ -177,6 +177,10 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
|||||||
assert.equal(manifest.quickstart.file, 'quickstart.sh');
|
assert.equal(manifest.quickstart.file, 'quickstart.sh');
|
||||||
assert.equal(manifest.upgradeReadiness.file, 'upgrade-readiness.sh');
|
assert.equal(manifest.upgradeReadiness.file, 'upgrade-readiness.sh');
|
||||||
assert.equal(manifest.upgradeRehearsal.file, 'upgrade-rehearsal.sh');
|
assert.equal(manifest.upgradeRehearsal.file, 'upgrade-rehearsal.sh');
|
||||||
|
assert.equal(
|
||||||
|
manifest.upgradeCutoverRehearsal.file,
|
||||||
|
'upgrade-cutover-rehearsal.sh',
|
||||||
|
);
|
||||||
const quickstart = path.join(paths.outputRoot, 'quickstart.sh');
|
const quickstart = path.join(paths.outputRoot, 'quickstart.sh');
|
||||||
const syntax = spawnSync('sh', ['-n', quickstart], { encoding: 'utf8' });
|
const syntax = spawnSync('sh', ['-n', quickstart], { encoding: 'utf8' });
|
||||||
assert.equal(syntax.status, 0, syntax.stderr);
|
assert.equal(syntax.status, 0, syntax.stderr);
|
||||||
@@ -186,6 +190,15 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
|||||||
/QingLong 3\.0 Local Alpha is active \(\$VARIANT, \$profile, \$ARCHITECTURE\)/,
|
/QingLong 3\.0 Local Alpha is active \(\$VARIANT, \$profile, \$ARCHITECTURE\)/,
|
||||||
);
|
);
|
||||||
assert.match(quickstartContents, /qinglong3-local-application:test-arm64/);
|
assert.match(quickstartContents, /qinglong3-local-application:test-arm64/);
|
||||||
|
const cutoverRehearsal = path.join(
|
||||||
|
paths.outputRoot,
|
||||||
|
'upgrade-cutover-rehearsal.sh',
|
||||||
|
);
|
||||||
|
const cutoverSyntax = spawnSync('sh', ['-n', cutoverRehearsal], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
assert.equal(cutoverSyntax.status, 0, cutoverSyntax.stderr);
|
||||||
|
assert.match(fs.readFileSync(cutoverRehearsal, 'utf8'), /VARIANT='headless'/);
|
||||||
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
|
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
|
||||||
assert.equal(report.compatible, true);
|
assert.equal(report.compatible, true);
|
||||||
assert.equal(report.sourceRevision, revision);
|
assert.equal(report.sourceRevision, revision);
|
||||||
@@ -199,6 +212,7 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
|||||||
'qinglong3-local-operator.cdx.json',
|
'qinglong3-local-operator.cdx.json',
|
||||||
'qinglong3-local-trial-kit-arm64.docker.tar',
|
'qinglong3-local-trial-kit-arm64.docker.tar',
|
||||||
'quickstart.sh',
|
'quickstart.sh',
|
||||||
|
'upgrade-cutover-rehearsal.sh',
|
||||||
'upgrade-readiness.sh',
|
'upgrade-readiness.sh',
|
||||||
'upgrade-rehearsal.sh',
|
'upgrade-rehearsal.sh',
|
||||||
'verification-evidence.json',
|
'verification-evidence.json',
|
||||||
@@ -235,6 +249,7 @@ test('materializes a distinct loopback Console trial kit without widening the he
|
|||||||
assert.equal(verification.gates.ownerCredentialPresentation, 'passed');
|
assert.equal(verification.gates.ownerCredentialPresentation, 'passed');
|
||||||
assert.equal(verification.gates.legacyUpgradeReadiness, 'passed');
|
assert.equal(verification.gates.legacyUpgradeReadiness, 'passed');
|
||||||
assert.equal(verification.gates.legacyUpgradeStage, 'passed');
|
assert.equal(verification.gates.legacyUpgradeStage, 'passed');
|
||||||
|
assert.equal(verification.gates.legacyUpgradeCutover, 'not_applicable');
|
||||||
const quickstartContents = fs.readFileSync(
|
const quickstartContents = fs.readFileSync(
|
||||||
path.join(paths.outputRoot, 'quickstart.sh'),
|
path.join(paths.outputRoot, 'quickstart.sh'),
|
||||||
'utf8',
|
'utf8',
|
||||||
@@ -246,6 +261,12 @@ test('materializes a distinct loopback Console trial kit without widening the he
|
|||||||
assert.match(quickstartContents, /Console: http:\/\/127\.0\.0\.1:5700\//);
|
assert.match(quickstartContents, /Console: http:\/\/127\.0\.0\.1:5700\//);
|
||||||
assert.match(quickstartContents, /alpha-first-automation/);
|
assert.match(quickstartContents, /alpha-first-automation/);
|
||||||
assert.match(quickstartContents, /do not expose the port on LAN/);
|
assert.match(quickstartContents, /do not expose the port on LAN/);
|
||||||
|
const cutoverContents = fs.readFileSync(
|
||||||
|
path.join(paths.outputRoot, 'upgrade-cutover-rehearsal.sh'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
assert.match(cutoverContents, /VARIANT='console'/);
|
||||||
|
assert.match(cutoverContents, /available only in the headless Trial Kit/);
|
||||||
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
|
const report = auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot });
|
||||||
assert.equal(report.compatible, true);
|
assert.equal(report.compatible, true);
|
||||||
assert.equal(report.variant, 'console');
|
assert.equal(report.variant, 'console');
|
||||||
@@ -257,6 +278,7 @@ test('materializes a distinct loopback Console trial kit without widening the he
|
|||||||
'qinglong3-local-console-trial-kit-arm64.docker.tar',
|
'qinglong3-local-console-trial-kit-arm64.docker.tar',
|
||||||
'qinglong3-local-operator.cdx.json',
|
'qinglong3-local-operator.cdx.json',
|
||||||
'quickstart.sh',
|
'quickstart.sh',
|
||||||
|
'upgrade-cutover-rehearsal.sh',
|
||||||
'upgrade-readiness.sh',
|
'upgrade-readiness.sh',
|
||||||
'upgrade-rehearsal.sh',
|
'upgrade-rehearsal.sh',
|
||||||
'verification-evidence.json',
|
'verification-evidence.json',
|
||||||
@@ -293,6 +315,7 @@ test('offline audit rejects archive, file-set, SBOM and verification mutation',
|
|||||||
'quickstart',
|
'quickstart',
|
||||||
'upgrade-readiness',
|
'upgrade-readiness',
|
||||||
'upgrade-rehearsal',
|
'upgrade-rehearsal',
|
||||||
|
'upgrade-cutover-rehearsal',
|
||||||
'sbom',
|
'sbom',
|
||||||
'verification',
|
'verification',
|
||||||
]) {
|
]) {
|
||||||
@@ -324,6 +347,11 @@ test('offline audit rejects archive, file-set, SBOM and verification mutation',
|
|||||||
path.join(paths.outputRoot, 'upgrade-rehearsal.sh'),
|
path.join(paths.outputRoot, 'upgrade-rehearsal.sh'),
|
||||||
'# drift\n',
|
'# drift\n',
|
||||||
);
|
);
|
||||||
|
} else if (mutation === 'upgrade-cutover-rehearsal') {
|
||||||
|
fs.appendFileSync(
|
||||||
|
path.join(paths.outputRoot, 'upgrade-cutover-rehearsal.sh'),
|
||||||
|
'# drift\n',
|
||||||
|
);
|
||||||
} else if (mutation === 'sbom') {
|
} else if (mutation === 'sbom') {
|
||||||
fs.copyFileSync(
|
fs.copyFileSync(
|
||||||
path.join(paths.outputRoot, 'qinglong3-local-application.cdx.json'),
|
path.join(paths.outputRoot, 'qinglong3-local-application.cdx.json'),
|
||||||
@@ -364,6 +392,7 @@ test('offline audit rejects a rehashed non-canonical quickstart', (t) => {
|
|||||||
'quickstart.sh',
|
'quickstart.sh',
|
||||||
'upgrade-readiness.sh',
|
'upgrade-readiness.sh',
|
||||||
'upgrade-rehearsal.sh',
|
'upgrade-rehearsal.sh',
|
||||||
|
'upgrade-cutover-rehearsal.sh',
|
||||||
'README.md',
|
'README.md',
|
||||||
'manifest.json',
|
'manifest.json',
|
||||||
];
|
];
|
||||||
@@ -618,7 +647,10 @@ exit 1
|
|||||||
assert.equal(summary.sqlite.activationDigest, 'd'.repeat(64));
|
assert.equal(summary.sqlite.activationDigest, 'd'.repeat(64));
|
||||||
assert.equal(summary.dataDirectory.manifestDigest, 'e'.repeat(64));
|
assert.equal(summary.dataDirectory.manifestDigest, 'e'.repeat(64));
|
||||||
const calls = fs.readFileSync(dockerLog, 'utf8');
|
const calls = fs.readFileSync(dockerLog, 'utf8');
|
||||||
assert.match(calls, /dst=\/var\/lib\/qinglong2,readonly/);
|
assert.match(
|
||||||
|
calls,
|
||||||
|
new RegExp(`dst=${legacyRoot.replaceAll('/', '\\/')},readonly`),
|
||||||
|
);
|
||||||
assert.match(calls, /--network none/);
|
assert.match(calls, /--network none/);
|
||||||
assert.match(calls, /--memory 128m --memory-swap 128m/);
|
assert.match(calls, /--memory 128m --memory-swap 128m/);
|
||||||
assert.doesNotMatch(calls, /cutover|target-start|legacy-rollback/);
|
assert.doesNotMatch(calls, /cutover|target-start|legacy-rollback/);
|
||||||
|
|||||||
Reference in New Issue
Block a user