mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): ship runnable local alpha quickstart
This commit is contained in:
@@ -631,6 +631,16 @@ jobs:
|
|||||||
--output="${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}"
|
--output="${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}"
|
||||||
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
||||||
--mode=audit --bundle="${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}"
|
--mode=audit --bundle="${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}"
|
||||||
|
QUICKSTART_CONTAINER="ql3-alpha-quickstart-${{ matrix.image_arch }}"
|
||||||
|
QUICKSTART_ROOT="${RUNNER_TEMP}/ql3-alpha-quickstart-${{ matrix.image_arch }}"
|
||||||
|
trap 'docker rm --force "${QUICKSTART_CONTAINER}" >/dev/null 2>&1 || true' EXIT
|
||||||
|
sh "${RUNNER_TEMP}/ql3-alpha-local-${{ matrix.image_arch }}/quickstart.sh" \
|
||||||
|
edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"
|
||||||
|
docker stop --time 30 "${QUICKSTART_CONTAINER}"
|
||||||
|
docker rm "${QUICKSTART_CONTAINER}"
|
||||||
|
test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"
|
||||||
|
test -s "${QUICKSTART_ROOT}/owner-delivery/credential-019f8680-143d-4000-8000-000000000021.ready.json"
|
||||||
|
trap - EXIT
|
||||||
- name: Upload the tested native Local Alpha trial kit
|
- name: Upload the tested native Local Alpha trial kit
|
||||||
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')
|
if: github.event_name == 'workflow_dispatch' && inputs.produce_alpha_artifacts && (inputs.alpha_artifact_scope == 'local' || inputs.alpha_artifact_scope == 'all')
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
最新增量证据(2026-08-28):
|
最新增量证据(2026-08-28):
|
||||||
|
|
||||||
|
- D-416/ADR-0511(已实现,首份真实 v3 Trial Kit 待授权):阶段产物的判定从“可下载、可验真”推进到“部署者可直接试运行”。Local Trial Kit schema 升为 `qinglong/alpha-local-trial-kit@v3`,每个 amd64/arm64 bundle 新增由唯一模板生成并绑定 archive、两个 exact image ID、source 和 architecture 的 `quickstart.sh`;离线 auditor 会重新渲染 canonical journey,拒绝即使重算 manifest/checksum 的本地脚本改写。目标路由/NAS 或单节点只需 POSIX shell、`sha256sum` 和 Docker,即可在一个尚不存在的隔离目录完成全包 checksum、镜像 load/identity 复核、128 MiB 短生命周期 operator fresh setup、首 Owner 建立,并按 Edge `128 MiB/64 PID` 或 Standalone `256 MiB/256 PID` 启动 Application、等待 `active` 后输出 logs/stop/remove 指令。显式 artifact job 在 upload 前必须执行将要上传的 exact quickstart、检查 SQLite/Owner delivery 并 graceful stop;普通 push 不生成大 archive。聚焦 materializer/milestone/stage/workflow 回归为 `26/26`,完整 backend 为 `1636 total / 1634 pass / 2 conditional skip / 0 fail`,18-package clean build/test 退出 0;package boundary 保持 `18 packages / singleSourcePackages=[] / shallowSourcePackages=[]`,Cluster dependency、122-module Edge import、镜像/版本与 14 档 Local artifact 均 compatible,基础 Edge/Standalone 仍为 `2,669,390 / 2,669,468 bytes`、325 files、58 modules。历史同源 arm64 镜像对已本地物化为 v3 八文件开发 bundle,184,648,192-byte archive 完成 checksum、canonical audit、load 与 exact identity;Docker Desktop 在 Owner directory ownership 处按既有 POSIX 不等价失败关闭,未冒充原生 Linux 成功,正式 native exact-script 证据由手动 artifact job 产生。该切片不安装宿主 Node.js,不把 Operator 变成 daemon,不增加镜像 layer、workspace package、端口或稳态 RSS,也不触碰 2.x/migration。当前产物是无外部 listener、AI-excluded 的 headless Alpha runtime,不冒充 2.x Web UI、公开 release、生产 HA 或 LTS;首份真实双架构 v3 下载物仍需维护者授权 `produce_alpha_artifacts=true`。
|
||||||
|
|
||||||
- D-415/ADR-0510(已实现,首份真实 stage index 待授权):二十天开发形成的 Local 与 Cluster 候选现在有统一、面向部署者的最外层阶段交付入口。新增 `qinglong/alpha-stage-index@v1` finalizer/auditor,仅在显式 `produce_alpha_artifacts=true + alpha_artifact_scope=all` 且 Local/Cluster milestone 都成功后运行;它重新离线审计两个索引,要求 version/source/workflow SHA/ref/run/attempt 完全一致,记录两个 milestone manifest digest,并为 amd64/arm64 写出机器可读选择:低配 Edge/Standalone 只下载一个 Local Trial Kit、稳态只运行 Application 且 Operator 短时启动;Cluster 下载 control/admin/worker,按需增加 control-ai。跨索引审计必须同时持有 stage 与两个 milestone,拒绝跨运行混用、内容/选择漂移和额外文件。该索引不重复十个大 archive,不改变 Local-only/Cluster-only 授权路径,也不冒充受保护 tag、GHCR immutable digest、签名、正式 OCI catalog、生产 deployment lock 或 HA;普通 push/PR 不生成实际阶段 artifact。实现不新增 workspace package、runtime dependency、镜像 layer、端口、daemon、timer、连接池或 Edge/Standalone 稳态 RSS。
|
- D-415/ADR-0510(已实现,首份真实 stage index 待授权):二十天开发形成的 Local 与 Cluster 候选现在有统一、面向部署者的最外层阶段交付入口。新增 `qinglong/alpha-stage-index@v1` finalizer/auditor,仅在显式 `produce_alpha_artifacts=true + alpha_artifact_scope=all` 且 Local/Cluster milestone 都成功后运行;它重新离线审计两个索引,要求 version/source/workflow SHA/ref/run/attempt 完全一致,记录两个 milestone manifest digest,并为 amd64/arm64 写出机器可读选择:低配 Edge/Standalone 只下载一个 Local Trial Kit、稳态只运行 Application 且 Operator 短时启动;Cluster 下载 control/admin/worker,按需增加 control-ai。跨索引审计必须同时持有 stage 与两个 milestone,拒绝跨运行混用、内容/选择漂移和额外文件。该索引不重复十个大 archive,不改变 Local-only/Cluster-only 授权路径,也不冒充受保护 tag、GHCR immutable digest、签名、正式 OCI catalog、生产 deployment lock 或 HA;普通 push/PR 不生成实际阶段 artifact。实现不新增 workspace package、runtime dependency、镜像 layer、端口、daemon、timer、连接池或 Edge/Standalone 稳态 RSS。
|
||||||
|
|
||||||
- D-414/ADR-0509(已实现,首份真实 Cluster milestone artifact 待授权):开发约二十天后,Cluster 阶段产物不再是八个由 workflow 内联 shell 自报 `passed` 的松散 archive。新增统一 `qinglong/alpha-cluster-image@v1` materializer/auditor,固定 control/control-ai/admin/worker 的 repository、OCI title、non-root user 和 amd64/arm64 identity;每个六文件 bundle 含单镜像 Docker archive、精确 SBOM、绑定 source/workflow SHA/ref/run/attempt/role/architecture/image ID 的 verification evidence、README、manifest 与 `SHA256SUMS`,下载后可无 Docker/网络离线复核闭合文件集和内容 digest。新增完整 CI 后置 `cluster-alpha-milestone` finalizer,重新下载八个 artifact,要求同一 version/source/run/attempt 且八个 image ID/archive digest/verification digest 互异,再上传 `qinglong/alpha-cluster-milestone@v1` 三文件索引;没有该索引的部分 archive 只是失败运行中间件。maturity 明确为 `cluster_integration_candidate_not_public_release`,适用于隔离 registry/K3s/Kubernetes 集成,不冒充受保护 tag、GHCR digest、签名、catalog 或生产 HA。实现不新增 workspace package、runtime dependency、镜像 layer、端口、daemon、timer、连接池或 Edge/Standalone 稳态 RSS;首份实际产物仍需维护者明确触发 `produce_alpha_artifacts=true + alpha_artifact_scope=cluster`。
|
- D-414/ADR-0509(已实现,首份真实 Cluster milestone artifact 待授权):开发约二十天后,Cluster 阶段产物不再是八个由 workflow 内联 shell 自报 `passed` 的松散 archive。新增统一 `qinglong/alpha-cluster-image@v1` materializer/auditor,固定 control/control-ai/admin/worker 的 repository、OCI title、non-root user 和 amd64/arm64 identity;每个六文件 bundle 含单镜像 Docker archive、精确 SBOM、绑定 source/workflow SHA/ref/run/attempt/role/architecture/image ID 的 verification evidence、README、manifest 与 `SHA256SUMS`,下载后可无 Docker/网络离线复核闭合文件集和内容 digest。新增完整 CI 后置 `cluster-alpha-milestone` finalizer,重新下载八个 artifact,要求同一 version/source/run/attempt 且八个 image ID/archive digest/verification digest 互异,再上传 `qinglong/alpha-cluster-milestone@v1` 三文件索引;没有该索引的部分 archive 只是失败运行中间件。maturity 明确为 `cluster_integration_candidate_not_public_release`,适用于隔离 registry/K3s/Kubernetes 集成,不冒充受保护 tag、GHCR digest、签名、catalog 或生产 HA。实现不新增 workspace package、runtime dependency、镜像 layer、端口、daemon、timer、连接池或 Edge/Standalone 稳态 RSS;首份实际产物仍需维护者明确触发 `produce_alpha_artifacts=true + alpha_artifact_scope=cluster`。
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# ADR-0511:可直接试运行的 Local Alpha Quickstart
|
||||||
|
|
||||||
|
- 状态:Accepted(首份实际 v3 Trial Kit 待维护者授权)
|
||||||
|
- 日期:2026-08-28
|
||||||
|
- 决策:D-416
|
||||||
|
- 关联:ADR-0193、ADR-0503、ADR-0504、ADR-0506、ADR-0508、ADR-0510
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
开发约二十天后,Local Alpha Trial Kit 已经能够闭合 Application/operator 镜像、SBOM、workflow evidence、manifest 和 checksum,并由双架构 milestone 与跨 Profile stage index 导航。但下载者仍需从长篇运维文档手工拼装 setup、首 Owner ceremony、Application config 和受限 `docker run`。现有 artifact 证明“镜像可验证”,还没有把已在 CI 通过的 fresh 用户旅程变成部署者拿到即可执行的阶段产物。
|
||||||
|
|
||||||
|
这对两类部署者都不合理:低配路由/NAS 不应为了试运行先安装 Node.js 24 或理解完整生产 deployment ceremony;普通单节点用户也不应复制多份含 mutation、UID/GID、resource limit 和 authority path 的 JSON。继续增加索引或证据而不关闭最后一公里,会把工程完整性误报为产品可用性。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
### 1. Local Trial Kit v3 必须携带 canonical quickstart
|
||||||
|
|
||||||
|
bundle schema 升为 `qinglong/alpha-local-trial-kit@v3`,闭合目录新增 `quickstart.sh`。materializer 从仓库内唯一模板写入当前 archive 名、Application/operator reference、exact image ID、source revision 和 architecture;manifest 增加 `quickstart` 文件记录,脚本同时进入 `SHA256SUMS`。
|
||||||
|
|
||||||
|
离线 auditor 不只检查脚本 byte/SHA-256,还根据 manifest 重新渲染 canonical journey 并逐字比较。因此本地重写脚本后即使同时重算 manifest 和 checksum,也不能通过源代码对应版本的 auditor。
|
||||||
|
|
||||||
|
### 2. 目标机只依赖 POSIX shell、sha256sum 和 Docker
|
||||||
|
|
||||||
|
quickstart 接受 `edge|standalone`、一个尚不存在的 canonical absolute data root,以及可选容器名。它按固定顺序执行:
|
||||||
|
|
||||||
|
1. 校验整个 bundle 的 `SHA256SUMS`,加载单一双镜像 archive;
|
||||||
|
2. 重新核对两个镜像的 ID、architecture、numeric user、source revision 和 operator lifecycle/network label;
|
||||||
|
3. 以当前宿主 UID:GID、无网络、只读 rootfs、drop-all capability、no-new-privileges 和 128 MiB operator 上限完成 fresh setup;
|
||||||
|
4. 通过短生命周期 operator 建立首个 Owner,敏感 delivery 只保存在新建 `0700` data root;
|
||||||
|
5. 以 Edge `128 MiB/64 PID` 或 Standalone `256 MiB/256 PID` 上限启动 Application,等待结构化 `active` 事件后才报告成功;
|
||||||
|
6. 输出 logs、graceful stop、container removal 和保留 data root 的明确命令。
|
||||||
|
|
||||||
|
脚本不需要宿主 Node.js、jq、Compose、网络访问或 root。operator 仍然每次只处理一个 command file 后退出,不成为 daemon 或 sidecar。
|
||||||
|
|
||||||
|
### 3. quickstart 本身必须经过 exact native artifact 门
|
||||||
|
|
||||||
|
显式 `workflow_dispatch + produce_alpha_artifacts=true` 的每个 Local architecture job 在 `create → audit` 后,必须从将要上传的目录执行生成出的 `quickstart.sh`,确认 SQLite、Owner delivery、Application active 与 graceful stop,再允许 upload。原有 Edge/Standalone live journey 继续保留;新增门证明的是“下载目录里的 exact 脚本可以驱动 exact 镜像”,不是另一个源码等价测试。
|
||||||
|
|
||||||
|
### 4. 阶段可用边界保持诚实
|
||||||
|
|
||||||
|
该 quickstart 只允许 fresh、隔离的新目录,不接受既有目录,不触碰 2.x 数据,不执行 migration/cutover,也不宣称生产升级、HA、公开签名或 LTS。当前 Application 是无外部 listener 的 headless runtime,能验证 3.0 SQLite、Owner authority、调度/插件运行基础和生命周期;它不是 2.x Web UI 的替代品,AI deployment 仍明确 excluded。
|
||||||
|
|
||||||
|
Cluster 节点继续使用 Cluster Integration Candidate 和 Kubernetes/CloudNativePG 路径,不能复用 Local quickstart。
|
||||||
|
|
||||||
|
## 被拒绝的替代方案
|
||||||
|
|
||||||
|
### 只在 README 增加更多手工命令
|
||||||
|
|
||||||
|
拒绝。文档无法保证 image identity、资源限制、authority path 和初始化顺序不漂移,也不能由 CI 执行下载者实际拿到的 journey。
|
||||||
|
|
||||||
|
### 只提供 Compose 文件
|
||||||
|
|
||||||
|
拒绝。Compose 可以描述常驻 Application,但不能安全替代短生命周期 setup 与首 Owner ceremony;把 operator 设为常驻 sidecar会扩大管理 authority。
|
||||||
|
|
||||||
|
### 提供 Node.js quickstart
|
||||||
|
|
||||||
|
拒绝。它会要求低配目标机额外安装与维护 Node.js,而镜像交付本来只要求容器运行时。
|
||||||
|
|
||||||
|
### 自动迁移现有 2.x 目录
|
||||||
|
|
||||||
|
拒绝。Alpha quickstart 没有 reconciliation、review、cutover 和 rollback authority,自动接管既有目录会越过已经冻结的迁移边界。
|
||||||
|
|
||||||
|
## 影响
|
||||||
|
|
||||||
|
- Local Alpha 从“可下载、可验真”前进到“可在 fresh 设备目录一条命令完成初始化并启动”;
|
||||||
|
- bundle 从七个文件增加为八个小文件,不新增镜像 layer、workspace package、runtime dependency、端口、后台 timer 或稳态 RSS;
|
||||||
|
- Edge/Standalone 继续共用同一镜像,差异只体现在 quickstart 的 memory/PID limit;
|
||||||
|
- `@v2` 仍是历史 verification-evidence 阶段格式,首份面向部署者的实际下载物必须使用 `@v3`;
|
||||||
|
- 首份真实 amd64/arm64 v3 Trial Kit、Local milestone 与 stage index 仍需维护者显式授权 artifact workflow,普通 push 不生成大归档。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- 单元测试覆盖 v3 八文件物化、POSIX shell 语法、exact image reference 注入、脚本/manifest/checksum 篡改,以及重算全部摘要后的非 canonical 脚本拒绝;
|
||||||
|
- Local milestone 与 Alpha stage index 回归证明 schema 升级没有放宽跨架构、跨 run/attempt 和跨 Profile 闭合;
|
||||||
|
- Local operator workflow 静态审计要求 `create → audit → quickstart → stop → upload` 顺序;
|
||||||
|
- 聚焦门为 `26/26`,完整 backend 为 `1636 total / 1634 pass / 2 conditional skip / 0 fail`,18-package clean build/test、package/dependency/Edge import、镜像/版本和 14 档 Local artifact audit 全部通过;
|
||||||
|
- 历史同源 arm64 Application/operator 已物化为 184,648,192-byte v3 开发 bundle并通过 checksum、canonical audit、load 与 exact identity;Docker Desktop 在 Owner POSIX directory ownership 处按已知不等价失败关闭,未被记录为 native quickstart 成功;
|
||||||
|
- 手动 artifact job 在原生 amd64/arm64 runner 上执行将要上传的 exact quickstart;首份实际运行结果待维护者授权触发后记录。
|
||||||
@@ -514,6 +514,7 @@
|
|||||||
| [ADR-0508](./ADR-0508-cross-architecture-local-alpha-milestone-closure.md) | 跨架构 Local Alpha 里程碑闭合 | Accepted(首份实际 milestone artifact 待维护者授权) |
|
| [ADR-0508](./ADR-0508-cross-architecture-local-alpha-milestone-closure.md) | 跨架构 Local Alpha 里程碑闭合 | Accepted(首份实际 milestone artifact 待维护者授权) |
|
||||||
| [ADR-0509](./ADR-0509-cluster-alpha-bundle-and-milestone-closure.md) | Cluster Alpha Bundle 与跨架构里程碑闭合 | Accepted(首份实际 Cluster milestone artifact 待维护者授权) |
|
| [ADR-0509](./ADR-0509-cluster-alpha-bundle-and-milestone-closure.md) | Cluster Alpha Bundle 与跨架构里程碑闭合 | Accepted(首份实际 Cluster milestone artifact 待维护者授权) |
|
||||||
| [ADR-0510](./ADR-0510-cross-profile-alpha-stage-delivery-index.md) | 跨 Profile Alpha 阶段交付索引 | Accepted(首份实际 stage index 待维护者授权) |
|
| [ADR-0510](./ADR-0510-cross-profile-alpha-stage-delivery-index.md) | 跨 Profile Alpha 阶段交付索引 | Accepted(首份实际 stage index 待维护者授权) |
|
||||||
|
| [ADR-0511](./ADR-0511-runnable-local-alpha-quickstart.md) | 可直接试运行的 Local Alpha Quickstart | Accepted(首份实际 v3 Trial Kit 待维护者授权) |
|
||||||
|
|
||||||
## 规则
|
## 规则
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
| 等级 | 面向对象 | 必须通过 | 当前用途 |
|
| 等级 | 面向对象 | 必须通过 | 当前用途 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Runtime Engineering Candidate | QingLong 开发者、设备兼容测试者 | 单个常驻镜像的 OS 漏洞策略、SBOM/库存、资源门和生命周期 | 验证 runtime 可加载、可启动;缺少管理制品时不能称用户 Alpha |
|
| Runtime Engineering Candidate | QingLong 开发者、设备兼容测试者 | 单个常驻镜像的 OS 漏洞策略、SBOM/库存、资源门和生命周期 | 验证 runtime 可加载、可启动;缺少管理制品时不能称用户 Alpha |
|
||||||
| Local Alpha Trial Kit | amd64/arm64 路由器、NAS、单机试用者 | 同源 Application + 短生命周期 operator、fresh setup/Owner/active/stop 完整旅程、SBOM/库存与资源门 | 一个去重 Docker archive 完成隔离 fresh 试运行;不承诺生产升级 |
|
| Local Alpha Trial Kit | amd64/arm64 路由器、NAS、单机试用者 | 同源 Application + 短生命周期 operator、exact quickstart、fresh setup/Owner/active/stop 完整旅程、SBOM/库存与资源门 | POSIX shell 一条命令从去重 Docker archive 完成隔离 fresh 试运行;不承诺生产升级 |
|
||||||
| Cluster Integration Candidate | amd64/arm64 集群测试节点 | OS 漏洞策略、SBOM 与镜像库存复核、non-root identity;Admin 额外通过产品 facade smoke | 导入隔离 registry/测试节点,进行多组件集成;不作为 production HA release |
|
| Cluster Integration Candidate | amd64/arm64 集群测试节点 | OS 漏洞策略、SBOM 与镜像库存复核、non-root identity;Admin 额外通过产品 facade smoke | 导入隔离 registry/测试节点,进行多组件集成;不作为 production HA release |
|
||||||
| Public Release Set | 生产用户 | 受保护 tag、六镜像 multi-arch digest(Local Application/operator + Cluster 四角色)、签名/attestation、私有发布证据、catalog、Local/Cluster 部署与回退闭环 | 尚未实际发布;只能由受保护 release workflow 生成 |
|
| Public Release Set | 生产用户 | 受保护 tag、六镜像 multi-arch digest(Local Application/operator + Cluster 四角色)、签名/attestation、私有发布证据、catalog、Local/Cluster 部署与回退闭环 | 尚未实际发布;只能由受保护 release workflow 生成 |
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
该本地 archive 不是新的 v2 Local Alpha Trial Kit。它在 ADR-0506 前生成,manifest v1 会无条件写入 `passed`,且 macOS Docker Desktop 因 bind-mount UID 映射无法对 exact 本地 archive 完成 Owner pepper 旅程;原生 CI 证明同源码实现,不自动证明另一个 archive 的 exact image bytes。它因此保留为工程候选,不冒充已获 workflow evidence 的用户 Alpha。
|
该本地 archive 不是新的 v2 Local Alpha Trial Kit。它在 ADR-0506 前生成,manifest v1 会无条件写入 `passed`,且 macOS Docker Desktop 因 bind-mount UID 映射无法对 exact 本地 archive 完成 Owner pepper 旅程;原生 CI 证明同源码实现,不自动证明另一个 archive 的 exact image bytes。它因此保留为工程候选,不冒充已获 workflow evidence 的用户 Alpha。
|
||||||
|
|
||||||
ADR-0506 现要求 `qinglong/alpha-local-trial-kit@v2` 额外包含 `verification-evidence.json`,绑定显式 `workflow_dispatch` 的 source、workflow SHA/ref、run/attempt、架构和两个 image ID。旧 `e3c05862` runtime-only archive、`2620be05` v1 Trial Kit 与 `4239464a` v1 archive 均为历史工程证据,不能通过 v2 auditor。下一项外部里程碑仍是维护者授权 `produce_alpha_artifacts=true`,由同一次原生 milestone job 生成 exact-image evidence 和双架构可下载 archive。
|
ADR-0506 的 `qinglong/alpha-local-trial-kit@v2` 首次增加了 source-bound `verification-evidence.json`;ADR-0511 进一步把当前格式升级为 `@v3`,新增不依赖宿主 Node.js 的 canonical `quickstart.sh`,把可验证镜像闭合为可执行的 fresh 用户旅程。旧 `e3c05862` runtime-only archive、`2620be05` v1 Trial Kit、`4239464a` v1 archive 与未携带 quickstart 的 v2 均为历史工程证据,不能通过 v3 auditor。下一项外部里程碑仍是维护者授权 `produce_alpha_artifacts=true`,由同一次原生 milestone job 生成并实际执行 exact-image 双架构可下载 archive。
|
||||||
|
|
||||||
## 生成
|
## 生成
|
||||||
|
|
||||||
@@ -43,7 +43,8 @@ ADR-0506 现要求 `qinglong/alpha-local-trial-kit@v2` 额外包含 `verificatio
|
|||||||
Local artifact 含:
|
Local artifact 含:
|
||||||
|
|
||||||
- 一个包含 Application 与短生命周期 operator 的 `qinglong3-local-trial-kit-<arch>.docker.tar`;共享 Node 基础层在 archive 中去重;
|
- 一个包含 Application 与短生命周期 operator 的 `qinglong3-local-trial-kit-<arch>.docker.tar`;共享 Node 基础层在 archive 中去重;
|
||||||
- schema 为 `qinglong/alpha-local-trial-kit@v2` 的 `manifest.json`,通过 `archive/images/sboms/readme/verification` 绑定版本、完整 source commit、架构、两个 image tag/image ID 与文件长度/SHA-256;
|
- schema 为 `qinglong/alpha-local-trial-kit@v3` 的 `manifest.json`,通过 `archive/images/sboms/quickstart/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;
|
||||||
- `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,它不替代正式签名;
|
||||||
- 与实际只读镜像 inventory 对账过的 CycloneDX SBOM;
|
- 与实际只读镜像 inventory 对账过的 CycloneDX SBOM;
|
||||||
- 面向 Local 用户的 README 与覆盖全部内容文件的 `SHA256SUMS`。
|
- 面向 Local 用户的 README 与覆盖全部内容文件的 `SHA256SUMS`。
|
||||||
@@ -54,7 +55,21 @@ Stage index 是 `qinglong/alpha-stage-index@v1` 三文件闭包。它重新审
|
|||||||
|
|
||||||
任何 required job 失败时不上传对应产物。artifact 名和 archive 内的 `ci-*` tag 都表示 commit-bound candidate,不能改名后冒充 `v3.x` release。
|
任何 required job 失败时不上传对应产物。artifact 名和 archive 内的 `ci-*` tag 都表示 commit-bound candidate,不能改名后冒充 `v3.x` release。
|
||||||
|
|
||||||
## 下载后验证与最小 smoke
|
## 下载后直接试运行
|
||||||
|
|
||||||
|
在同架构原生 Linux Docker 主机上进入解压后的 Local artifact 目录,使用一个尚不存在的
|
||||||
|
隔离目录:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh quickstart.sh edge /opt/qinglong3-alpha-data
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以选择 `standalone` 和自定义容器名。quickstart 会先执行 `SHA256SUMS`,再核对 exact
|
||||||
|
镜像身份并完成 fresh setup、首 Owner 与 Application active;成功后输出 logs、stop 和
|
||||||
|
remove 命令。当前是无外部 listener、AI-excluded 的 headless runtime 阶段成果,不是
|
||||||
|
2.x Web UI 的替代品。
|
||||||
|
|
||||||
|
## 手工验证与最小 smoke
|
||||||
|
|
||||||
在同架构 Linux Docker 主机上进入解压后的 artifact 目录:
|
在同架构 Linux Docker 主机上进入解压后的 artifact 目录:
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ sha256sum --check SHA256SUMS
|
|||||||
|
|
||||||
`manifest.json` 必须满足:
|
`manifest.json` 必须满足:
|
||||||
|
|
||||||
- `schema` 为 `qinglong/alpha-local-trial-kit@v2`;
|
- `schema` 为 `qinglong/alpha-local-trial-kit@v3`;
|
||||||
- `sourceRevision` 是你准备试用的完整 40 位 commit;
|
- `sourceRevision` 是你准备试用的完整 40 位 commit;
|
||||||
- `architecture` 与主机相同;
|
- `architecture` 与主机相同;
|
||||||
- `maturity` 为 `alpha_candidate_not_public_release`。
|
- `maturity` 为 `alpha_candidate_not_public_release`。
|
||||||
@@ -40,7 +40,37 @@ node scripts/ql3-local-alpha-trial-kit-bundle.cjs \
|
|||||||
|
|
||||||
任一校验失败都不要加载或运行 archive。
|
任一校验失败都不要加载或运行 archive。
|
||||||
|
|
||||||
## 加载与最小 smoke
|
## 一条命令完成 Fresh 试运行
|
||||||
|
|
||||||
|
v3 bundle 内的 `quickstart.sh` 不依赖宿主 Node.js、jq 或 Compose,只需要 POSIX
|
||||||
|
shell、`sha256sum` 和已启动的 Docker。必须选择一个尚不存在、与 2.x/生产数据完全
|
||||||
|
隔离的绝对路径:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh quickstart.sh edge /opt/qinglong3-alpha-data
|
||||||
|
```
|
||||||
|
|
||||||
|
资源较充足的单节点可以把 `edge` 改为 `standalone`。第三个可选参数用于指定容器名:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh quickstart.sh standalone /srv/qinglong3-alpha-data ql3-alpha-standalone
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会自动执行全包 checksum、加载 archive、核对 exact image ID/source/architecture、
|
||||||
|
以当前 UID:GID 和无网络的短生命周期 operator 完成 fresh setup 与首 Owner 建立,随后按
|
||||||
|
Profile 资源上限启动 Application。只有容器日志出现结构化 `active` 事件才返回成功。
|
||||||
|
Owner delivery 保留在新数据目录的 `owner-delivery/`,operator command 结果保留在
|
||||||
|
`results/`;两者都位于 `0700` 私有根内,不会打印 Secret 到终端。
|
||||||
|
|
||||||
|
成功输出会给出当前容器的 logs、graceful stop 和 remove 命令。停止/删除容器不会自动
|
||||||
|
删除数据目录;确认不再需要诊断后由操作者显式删除该 fresh 测试目录。脚本拒绝既有目录,
|
||||||
|
不能用于升级、迁移或接管 2.x。
|
||||||
|
|
||||||
|
当前 Application 是无外部 listener、AI-excluded 的 headless Alpha runtime。该试运行可
|
||||||
|
验证 3.0 SQLite、Owner authority、调度/插件基础与生命周期,但还不是 2.x Web UI 的
|
||||||
|
可替代版本。
|
||||||
|
|
||||||
|
## 手工加载与最小 smoke
|
||||||
|
|
||||||
从 `manifest.json.archive.file` 找到 archive 后加载:
|
从 `manifest.json.archive.file` 找到 archive 后加载:
|
||||||
|
|
||||||
@@ -66,7 +96,7 @@ docker run --rm --read-only --network none --cap-drop ALL \
|
|||||||
|
|
||||||
## Fresh 试运行边界
|
## Fresh 试运行边界
|
||||||
|
|
||||||
完整 fresh setup、首 Owner ceremony、Application active、SIGTERM drain、SQLite integrity 和原生 cancellation 必须在 `verification-evidence.json` 指向的同架构 milestone job 中验证。实际部署时仍必须使用独立目录,并让 operator 以最终数据文件 POSIX owner 的 UID/GID 运行;operator 默认无网络且每次只执行一个命令后退出,不应作为 sidecar 或 daemon 常驻。
|
完整 fresh setup、首 Owner ceremony、Application active、SIGTERM drain、SQLite integrity 和原生 cancellation 必须在 `verification-evidence.json` 指向的同架构 milestone job 中验证。v3 artifact job 还必须从将要上传的目录实际执行 `quickstart.sh` 并完成 graceful stop。实际部署时仍必须使用独立目录,并让 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 的容量承诺。
|
||||||
|
|
||||||
|
|||||||
@@ -10,18 +10,24 @@ 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@v2';
|
const SCHEMA = 'qinglong/alpha-local-trial-kit@v3';
|
||||||
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v1';
|
const VERIFICATION_SCHEMA = 'qinglong/alpha-local-trial-kit-verification@v1';
|
||||||
|
const QUICKSTART_TEMPLATE = path.join(
|
||||||
|
DEFAULT_ROOT,
|
||||||
|
'scripts/templates/ql3-local-alpha-quickstart.sh',
|
||||||
|
);
|
||||||
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
const ARCHITECTURES = Object.freeze(['amd64', 'arm64']);
|
||||||
const ARCHIVE_MIN_BYTES = 1024;
|
const ARCHIVE_MIN_BYTES = 1024;
|
||||||
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
const MAX_JSON_BYTES = 4 * 1024 * 1024;
|
||||||
const MAX_README_BYTES = 512 * 1024;
|
const MAX_README_BYTES = 512 * 1024;
|
||||||
|
const MAX_QUICKSTART_BYTES = 256 * 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({
|
||||||
applicationSbom: 'qinglong3-local-application.cdx.json',
|
applicationSbom: 'qinglong3-local-application.cdx.json',
|
||||||
operatorSbom: 'qinglong3-local-operator.cdx.json',
|
operatorSbom: 'qinglong3-local-operator.cdx.json',
|
||||||
verificationEvidence: 'verification-evidence.json',
|
verificationEvidence: 'verification-evidence.json',
|
||||||
|
quickstart: 'quickstart.sh',
|
||||||
readme: 'README.md',
|
readme: 'README.md',
|
||||||
manifest: 'manifest.json',
|
manifest: 'manifest.json',
|
||||||
checksums: 'SHA256SUMS',
|
checksums: 'SHA256SUMS',
|
||||||
@@ -148,9 +154,7 @@ function saveDockerImages(images, archivePath) {
|
|||||||
function validateImageReference(value, label) {
|
function validateImageReference(value, label) {
|
||||||
if (
|
if (
|
||||||
typeof value !== 'string' ||
|
typeof value !== 'string' ||
|
||||||
value.length < 3 ||
|
!/^[A-Za-z0-9][A-Za-z0-9._:/@-]{2,255}$/u.test(value)
|
||||||
value.length > 256 ||
|
|
||||||
/[\s\0]/u.test(value)
|
|
||||||
) {
|
) {
|
||||||
fail(`${label} image reference is invalid`);
|
fail(`${label} image reference is invalid`);
|
||||||
}
|
}
|
||||||
@@ -383,6 +387,34 @@ function archiveName(architecture) {
|
|||||||
return `qinglong3-local-trial-kit-${architecture}.docker.tar`;
|
return `qinglong3-local-trial-kit-${architecture}.docker.tar`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderQuickstart(identity) {
|
||||||
|
const template = fs.readFileSync(
|
||||||
|
assertCanonicalFile(
|
||||||
|
QUICKSTART_TEMPLATE,
|
||||||
|
MAX_QUICKSTART_BYTES,
|
||||||
|
'quickstart 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,
|
||||||
|
});
|
||||||
|
let rendered = template;
|
||||||
|
for (const [token, value] of Object.entries(replacements)) {
|
||||||
|
rendered = rendered.replaceAll(token, value);
|
||||||
|
}
|
||||||
|
if (/@@[A-Z_]+@@/u.test(rendered)) {
|
||||||
|
fail('quickstart 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);
|
||||||
@@ -541,8 +573,19 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
normalized.readme,
|
normalized.readme,
|
||||||
path.join(normalized.outputRoot, FILES.readme),
|
path.join(normalized.outputRoot, FILES.readme),
|
||||||
);
|
);
|
||||||
|
const manifestIdentity = {
|
||||||
|
sourceRevision: normalized.sourceRevision,
|
||||||
|
architecture: normalized.architecture,
|
||||||
|
archive: { file: archive },
|
||||||
|
images: { application, operator },
|
||||||
|
};
|
||||||
|
writeExclusive(
|
||||||
|
path.join(normalized.outputRoot, FILES.quickstart),
|
||||||
|
renderQuickstart(manifestIdentity),
|
||||||
|
0o700,
|
||||||
|
);
|
||||||
const manifest = {
|
const manifest = {
|
||||||
schemaVersion: 3,
|
schemaVersion: 4,
|
||||||
schema: SCHEMA,
|
schema: SCHEMA,
|
||||||
maturity: 'alpha_candidate_not_public_release',
|
maturity: 'alpha_candidate_not_public_release',
|
||||||
product: 'local',
|
product: 'local',
|
||||||
@@ -555,6 +598,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
application: fileRecord(normalized.outputRoot, FILES.applicationSbom),
|
application: fileRecord(normalized.outputRoot, FILES.applicationSbom),
|
||||||
operator: fileRecord(normalized.outputRoot, FILES.operatorSbom),
|
operator: fileRecord(normalized.outputRoot, FILES.operatorSbom),
|
||||||
},
|
},
|
||||||
|
quickstart: fileRecord(normalized.outputRoot, FILES.quickstart),
|
||||||
readme: fileRecord(normalized.outputRoot, FILES.readme),
|
readme: fileRecord(normalized.outputRoot, FILES.readme),
|
||||||
verification: fileRecord(
|
verification: fileRecord(
|
||||||
normalized.outputRoot,
|
normalized.outputRoot,
|
||||||
@@ -570,6 +614,7 @@ function createLocalAlphaTrialKit(options, adapters = {}) {
|
|||||||
FILES.applicationSbom,
|
FILES.applicationSbom,
|
||||||
FILES.operatorSbom,
|
FILES.operatorSbom,
|
||||||
FILES.verificationEvidence,
|
FILES.verificationEvidence,
|
||||||
|
FILES.quickstart,
|
||||||
FILES.readme,
|
FILES.readme,
|
||||||
FILES.manifest,
|
FILES.manifest,
|
||||||
];
|
];
|
||||||
@@ -637,10 +682,11 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
'archive',
|
'archive',
|
||||||
'images',
|
'images',
|
||||||
'sboms',
|
'sboms',
|
||||||
|
'quickstart',
|
||||||
'readme',
|
'readme',
|
||||||
'verification',
|
'verification',
|
||||||
]) ||
|
]) ||
|
||||||
manifest.schemaVersion !== 3 ||
|
manifest.schemaVersion !== 4 ||
|
||||||
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' ||
|
||||||
@@ -673,6 +719,19 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
FILES.verificationEvidence,
|
FILES.verificationEvidence,
|
||||||
bundleRoot,
|
bundleRoot,
|
||||||
);
|
);
|
||||||
|
validateFileRecord(manifest.quickstart, FILES.quickstart, bundleRoot);
|
||||||
|
const expectedQuickstart = renderQuickstart(manifest);
|
||||||
|
const actualQuickstart = fs.readFileSync(
|
||||||
|
assertCanonicalFile(
|
||||||
|
path.join(bundleRoot, FILES.quickstart),
|
||||||
|
MAX_QUICKSTART_BYTES,
|
||||||
|
'quickstart',
|
||||||
|
),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
if (actualQuickstart !== expectedQuickstart) {
|
||||||
|
fail('quickstart differs from the canonical deployment journey');
|
||||||
|
}
|
||||||
validateFileRecord(manifest.readme, FILES.readme, bundleRoot);
|
validateFileRecord(manifest.readme, FILES.readme, bundleRoot);
|
||||||
validateOfflineSbom(
|
validateOfflineSbom(
|
||||||
readBoundedJson(
|
readBoundedJson(
|
||||||
@@ -707,6 +766,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
FILES.applicationSbom,
|
FILES.applicationSbom,
|
||||||
FILES.operatorSbom,
|
FILES.operatorSbom,
|
||||||
FILES.verificationEvidence,
|
FILES.verificationEvidence,
|
||||||
|
FILES.quickstart,
|
||||||
expectedArchive,
|
expectedArchive,
|
||||||
].sort();
|
].sort();
|
||||||
const actualFiles = fs
|
const actualFiles = fs
|
||||||
@@ -726,6 +786,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
FILES.applicationSbom,
|
FILES.applicationSbom,
|
||||||
FILES.operatorSbom,
|
FILES.operatorSbom,
|
||||||
FILES.verificationEvidence,
|
FILES.verificationEvidence,
|
||||||
|
FILES.quickstart,
|
||||||
FILES.readme,
|
FILES.readme,
|
||||||
FILES.manifest,
|
FILES.manifest,
|
||||||
];
|
];
|
||||||
@@ -746,6 +807,7 @@ function auditLocalAlphaTrialKit(options) {
|
|||||||
archiveSha256: manifest.archive.sha256,
|
archiveSha256: manifest.archive.sha256,
|
||||||
applicationImageId: manifest.images.application.id,
|
applicationImageId: manifest.images.application.id,
|
||||||
operatorImageId: manifest.images.operator.id,
|
operatorImageId: manifest.images.operator.id,
|
||||||
|
quickstartSha256: manifest.quickstart.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,
|
||||||
|
|||||||
@@ -199,6 +199,9 @@ function auditWorkflow(contents, findings) {
|
|||||||
'--mode=record-verification',
|
'--mode=record-verification',
|
||||||
'--mode=create',
|
'--mode=create',
|
||||||
'--mode=audit',
|
'--mode=audit',
|
||||||
|
'/quickstart.sh" \\\n edge "${QUICKSTART_ROOT}" "${QUICKSTART_CONTAINER}"',
|
||||||
|
'docker stop --time 30 "${QUICKSTART_CONTAINER}"',
|
||||||
|
'test -s "${QUICKSTART_ROOT}/qinglong3.sqlite"',
|
||||||
'--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json"',
|
'--application-sbom="${RUNNER_TEMP}/ql3-local-application.cdx.json"',
|
||||||
'--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"',
|
||||||
@@ -222,6 +225,7 @@ function auditWorkflow(contents, findings) {
|
|||||||
'--mode=record-verification',
|
'--mode=record-verification',
|
||||||
'--mode=create',
|
'--mode=create',
|
||||||
'--mode=audit',
|
'--mode=audit',
|
||||||
|
'/quickstart.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,162 @@
|
|||||||
|
#!/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@@'
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '%s\n' "QingLong Local Alpha quickstart failed: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
printf '%s\n' \
|
||||||
|
'usage: sh quickstart.sh edge|standalone /absolute/new/data-root [container-name]' >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
[ "$#" -ge 2 ] && [ "$#" -le 3 ] || usage
|
||||||
|
profile=$1
|
||||||
|
data_root=$2
|
||||||
|
container_name=${3:-ql3-alpha-local}
|
||||||
|
|
||||||
|
case "$profile" in
|
||||||
|
edge)
|
||||||
|
memory=128m
|
||||||
|
pids=64
|
||||||
|
;;
|
||||||
|
standalone)
|
||||||
|
memory=256m
|
||||||
|
pids=256
|
||||||
|
;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$data_root" in
|
||||||
|
/|*[!A-Za-z0-9_./-]*|*'/../'*|*'/./'*|*'/..'|*'/.'|*'//'*|*/)
|
||||||
|
fail 'data root must be a canonical absolute path using A-Z, a-z, 0-9, _, ., / or -'
|
||||||
|
;;
|
||||||
|
/*) ;;
|
||||||
|
*) fail 'data root must be absolute' ;;
|
||||||
|
esac
|
||||||
|
case "$container_name" in
|
||||||
|
''|[_.-]*|*[!A-Za-z0-9_.-]*) fail 'container name is invalid' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 || fail 'docker is required'
|
||||||
|
command -v sha256sum >/dev/null 2>&1 || fail 'sha256sum is required'
|
||||||
|
command -v grep >/dev/null 2>&1 || fail 'grep is required'
|
||||||
|
|
||||||
|
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
|
||||||
|
parent=${data_root%/*}
|
||||||
|
[ -n "$parent" ] || parent=/
|
||||||
|
[ -d "$parent" ] || fail 'data root parent does not exist'
|
||||||
|
parent_real=$(CDPATH= cd -- "$parent" && pwd -P)
|
||||||
|
[ "$parent_real/${data_root##*/}" = "$data_root" ] || fail 'data root parent is not canonical'
|
||||||
|
[ ! -e "$data_root" ] || fail 'data root must not already exist'
|
||||||
|
|
||||||
|
(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'
|
||||||
|
|
||||||
|
old_umask=$(umask)
|
||||||
|
umask 077
|
||||||
|
mkdir -m 0700 "$data_root"
|
||||||
|
for directory in owner-peppers owner-pepper-backup owner-delivery receipts artifacts plugin-staging plugin-activation results; do
|
||||||
|
mkdir -m 0700 "$data_root/$directory"
|
||||||
|
done
|
||||||
|
|
||||||
|
cat >"$data_root/setup.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"local.setup.prepare","options":{"deploymentRoot":"/var/lib/qinglong3","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","profile":"$profile","ownerPepperKeyringDirectory":"/var/lib/qinglong3/owner-peppers","ownerPepperBackupDirectory":"/var/lib/qinglong3/owner-pepper-backup","ownerPepperKeyId":"owner-v1","localSecretKeyringPath":"/var/lib/qinglong3/local-secret-keyring.json","busyTimeoutMs":100},"request":{"registerMutationId":"019f8680-143d-4000-8000-000000000011","activateMutationId":"019f8680-143d-4000-8000-000000000012","registeredAtMs":1785254400000,"activatedAtMs":1785254400001}}
|
||||||
|
EOF
|
||||||
|
cat >"$data_root/owner-provision.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"owner.identity.provision","options":{"deploymentRoot":"/var/lib/qinglong3","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","pepperPath":"/var/lib/qinglong3/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"/var/lib/qinglong3/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"mutationId":"019f8680-143d-4000-8000-000000000021","requestId":"alpha-trial-owner-provision"}}
|
||||||
|
EOF
|
||||||
|
cat >"$data_root/owner-challenge.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"owner.challenge.issue","options":{"deploymentRoot":"/var/lib/qinglong3","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","pepperPath":"/var/lib/qinglong3/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"/var/lib/qinglong3/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"projectId":"default","mutationId":"019f8680-143d-4000-8000-000000000022","requestId":"alpha-trial-owner-challenge"}}
|
||||||
|
EOF
|
||||||
|
cat >"$data_root/owner-claim.json" <<EOF
|
||||||
|
{"schemaVersion":1,"operation":"owner.claim.from-deliveries","options":{"deploymentRoot":"/var/lib/qinglong3","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","pepperPath":"/var/lib/qinglong3/owner-peppers/b3duZXItdjE.pepper","pepperKeyId":"owner-v1","secretDeliveryDirectory":"/var/lib/qinglong3/owner-delivery","profile":"$profile","busyTimeoutMs":100},"request":{"projectId":"default","mutationId":"019f8680-143d-4000-8000-000000000023","requestId":"alpha-trial-owner-claim","credentialMutationId":"019f8680-143d-4000-8000-000000000021","challengeMutationId":"019f8680-143d-4000-8000-000000000022"}}
|
||||||
|
EOF
|
||||||
|
cat >"$data_root/local-application.json" <<EOF
|
||||||
|
{"schema":"qinglong/local-application-process@v2","instanceId":"alpha-trial-local","profile":"$profile","storage":{"mode":"fresh","databasePath":"/var/lib/qinglong3/qinglong3.sqlite","busyTimeoutMs":100},"runtime":{"receiptRoot":"/var/lib/qinglong3/receipts","artifactRoot":"/var/lib/qinglong3/artifacts","secretKeyringPath":"/var/lib/qinglong3/local-secret-keyring.json"},"pluginPackages":{"stagingRoot":"/var/lib/qinglong3/plugin-staging","activationRoot":"/var/lib/qinglong3/plugin-activation","recoverySource":{"mode":"disabled"},"pageSize":4,"maxPages":4,"taskPublicationPageSize":4,"taskPublicationMaxPages":4},"ai":{"deployment":"excluded"}}
|
||||||
|
EOF
|
||||||
|
chmod 0600 "$data_root"/*.json
|
||||||
|
|
||||||
|
uid=$(id -u)
|
||||||
|
gid=$(id -g)
|
||||||
|
run_operator() {
|
||||||
|
command_name=$1
|
||||||
|
command_file=$2
|
||||||
|
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=$data_root,dst=/var/lib/qinglong3" \
|
||||||
|
"$OPERATOR_IMAGE" "$command_name" run \
|
||||||
|
--command-file "/var/lib/qinglong3/$command_file" \
|
||||||
|
>"$data_root/results/$command_file.result.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_operator setup setup.json
|
||||||
|
grep -q '"status":"prepared"' "$data_root/results/setup.json.result.json" || fail 'fresh setup did not report prepared'
|
||||||
|
run_operator owner owner-provision.json
|
||||||
|
grep -q '"status":"inserted"' "$data_root/results/owner-provision.json.result.json" || fail 'Owner credential provisioning did not report inserted'
|
||||||
|
run_operator owner owner-challenge.json
|
||||||
|
grep -q '"status":"inserted"' "$data_root/results/owner-challenge.json.result.json" || fail 'Owner challenge did not report inserted'
|
||||||
|
run_operator owner owner-claim.json
|
||||||
|
grep -q '"status":"inserted"' "$data_root/results/owner-claim.json.result.json" || fail 'Owner claim did not report inserted'
|
||||||
|
grep -q '"role":"owner"' "$data_root/results/owner-claim.json.result.json" || fail 'Owner claim did not establish the owner role'
|
||||||
|
|
||||||
|
ready=0
|
||||||
|
cleanup() {
|
||||||
|
if [ "$ready" -ne 1 ]; then
|
||||||
|
docker rm --force "$container_name" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
trap 'exit 130' HUP INT TERM
|
||||||
|
|
||||||
|
container_id=$(docker run --detach --name "$container_name" \
|
||||||
|
--restart unless-stopped --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=$data_root,dst=/var/lib/qinglong3" \
|
||||||
|
"$APPLICATION_IMAGE" --config /var/lib/qinglong3/local-application.json)
|
||||||
|
printf '%s\n' "$container_id" >"$data_root/container.id"
|
||||||
|
chmod 0600 "$data_root/container.id"
|
||||||
|
|
||||||
|
attempt=0
|
||||||
|
while [ "$attempt" -lt 45 ]; do
|
||||||
|
if docker logs "$container_name" 2>&1 | grep -q '"event":"active"'; then
|
||||||
|
ready=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
running=$(docker inspect --format '{{.State.Running}}' "$container_name" 2>/dev/null || true)
|
||||||
|
[ "$running" = true ] || fail 'application stopped before becoming active'
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
[ "$ready" -eq 1 ] || fail 'application did not become active within 45 seconds'
|
||||||
|
umask "$old_umask"
|
||||||
|
|
||||||
|
printf '%s\n' \
|
||||||
|
"QingLong 3.0 Local Alpha is active ($profile, $ARCHITECTURE)." \
|
||||||
|
"Data root: $data_root" \
|
||||||
|
"Owner deliveries: $data_root/owner-delivery" \
|
||||||
|
"Logs: docker logs $container_name" \
|
||||||
|
"Stop: docker stop --time 30 $container_name" \
|
||||||
|
"Remove container: docker rm $container_name" \
|
||||||
|
'The fresh data root is retained until you remove it explicitly.'
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
@@ -10,6 +11,7 @@ const {
|
|||||||
createLocalAlphaTrialKit,
|
createLocalAlphaTrialKit,
|
||||||
createLocalAlphaTrialKitVerificationEvidence,
|
createLocalAlphaTrialKitVerificationEvidence,
|
||||||
parseArguments,
|
parseArguments,
|
||||||
|
sha256File,
|
||||||
} = require('../../scripts/ql3-local-alpha-trial-kit-bundle.cjs');
|
} = require('../../scripts/ql3-local-alpha-trial-kit-bundle.cjs');
|
||||||
const {
|
const {
|
||||||
createClusterImageSbom,
|
createClusterImageSbom,
|
||||||
@@ -148,13 +150,23 @@ function adapters(overrides = {}) {
|
|||||||
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@v2');
|
assert.equal(manifest.schema, 'qinglong/alpha-local-trial-kit@v3');
|
||||||
assert.equal(manifest.sourceRevision, revision);
|
assert.equal(manifest.sourceRevision, revision);
|
||||||
assert.equal(manifest.architecture, 'arm64');
|
assert.equal(manifest.architecture, 'arm64');
|
||||||
assert.equal(manifest.images.application.architecture, 'arm64');
|
assert.equal(manifest.images.application.architecture, 'arm64');
|
||||||
assert.equal(manifest.images.operator.architecture, 'arm64');
|
assert.equal(manifest.images.operator.architecture, 'arm64');
|
||||||
assert.notEqual(manifest.images.application.id, manifest.images.operator.id);
|
assert.notEqual(manifest.images.application.id, manifest.images.operator.id);
|
||||||
assert.equal(manifest.verification.file, 'verification-evidence.json');
|
assert.equal(manifest.verification.file, 'verification-evidence.json');
|
||||||
|
assert.equal(manifest.quickstart.file, 'quickstart.sh');
|
||||||
|
const quickstart = path.join(paths.outputRoot, 'quickstart.sh');
|
||||||
|
const syntax = spawnSync('sh', ['-n', quickstart], { encoding: 'utf8' });
|
||||||
|
assert.equal(syntax.status, 0, syntax.stderr);
|
||||||
|
const quickstartContents = fs.readFileSync(quickstart, 'utf8');
|
||||||
|
assert.match(
|
||||||
|
quickstartContents,
|
||||||
|
/QingLong 3\.0 Local Alpha is active \(\$profile, \$ARCHITECTURE\)/,
|
||||||
|
);
|
||||||
|
assert.match(quickstartContents, /qinglong3-local-application:test-arm64/);
|
||||||
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);
|
||||||
@@ -166,6 +178,7 @@ test('materializes and offline-audits one closed two-image trial kit', (t) => {
|
|||||||
'qinglong3-local-application.cdx.json',
|
'qinglong3-local-application.cdx.json',
|
||||||
'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',
|
||||||
'verification-evidence.json',
|
'verification-evidence.json',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -194,7 +207,13 @@ test('fails closed and removes a partial output on incompatible image identity',
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('offline audit rejects archive, file-set, SBOM and verification mutation', (t) => {
|
test('offline audit rejects archive, file-set, SBOM and verification mutation', (t) => {
|
||||||
for (const mutation of ['archive', 'extra', 'sbom', 'verification']) {
|
for (const mutation of [
|
||||||
|
'archive',
|
||||||
|
'extra',
|
||||||
|
'quickstart',
|
||||||
|
'sbom',
|
||||||
|
'verification',
|
||||||
|
]) {
|
||||||
const paths = fixture(t);
|
const paths = fixture(t);
|
||||||
paths.outputRoot = path.join(paths.fixtureRoot, `bundle-${mutation}`);
|
paths.outputRoot = path.join(paths.fixtureRoot, `bundle-${mutation}`);
|
||||||
createLocalAlphaTrialKit(createOptions(paths), adapters());
|
createLocalAlphaTrialKit(createOptions(paths), adapters());
|
||||||
@@ -208,6 +227,11 @@ test('offline audit rejects archive, file-set, SBOM and verification mutation',
|
|||||||
);
|
);
|
||||||
} else if (mutation === 'extra') {
|
} else if (mutation === 'extra') {
|
||||||
fs.writeFileSync(path.join(paths.outputRoot, 'credential.txt'), 'secret');
|
fs.writeFileSync(path.join(paths.outputRoot, 'credential.txt'), 'secret');
|
||||||
|
} else if (mutation === 'quickstart') {
|
||||||
|
fs.appendFileSync(
|
||||||
|
path.join(paths.outputRoot, 'quickstart.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'),
|
||||||
@@ -227,6 +251,143 @@ test('offline audit rejects archive, file-set, SBOM and verification mutation',
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('offline audit rejects a rehashed non-canonical quickstart', (t) => {
|
||||||
|
const paths = fixture(t);
|
||||||
|
createLocalAlphaTrialKit(createOptions(paths), adapters());
|
||||||
|
const quickstart = path.join(paths.outputRoot, 'quickstart.sh');
|
||||||
|
fs.appendFileSync(quickstart, '# locally rewritten\n');
|
||||||
|
const manifestPath = path.join(paths.outputRoot, 'manifest.json');
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||||
|
manifest.quickstart = {
|
||||||
|
file: 'quickstart.sh',
|
||||||
|
sha256: sha256File(quickstart),
|
||||||
|
bytes: fs.statSync(quickstart).size,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
||||||
|
const checkedFiles = [
|
||||||
|
'qinglong3-local-trial-kit-arm64.docker.tar',
|
||||||
|
'qinglong3-local-application.cdx.json',
|
||||||
|
'qinglong3-local-operator.cdx.json',
|
||||||
|
'verification-evidence.json',
|
||||||
|
'quickstart.sh',
|
||||||
|
'README.md',
|
||||||
|
'manifest.json',
|
||||||
|
];
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(paths.outputRoot, 'SHA256SUMS'),
|
||||||
|
`${checkedFiles
|
||||||
|
.map(
|
||||||
|
(name) =>
|
||||||
|
`${sha256File(path.join(paths.outputRoot, name)).slice(7)} ${name}`,
|
||||||
|
)
|
||||||
|
.join('\n')}\n`,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => auditLocalAlphaTrialKit({ bundleRoot: paths.outputRoot }),
|
||||||
|
/quickstart differs from the canonical deployment journey/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('generated quickstart drives the closed fresh Edge journey', (t) => {
|
||||||
|
const paths = fixture(t);
|
||||||
|
createLocalAlphaTrialKit(createOptions(paths), adapters());
|
||||||
|
const fakeBin = path.join(paths.fixtureRoot, 'fake-bin');
|
||||||
|
fs.mkdirSync(fakeBin);
|
||||||
|
const dockerLog = path.join(paths.fixtureRoot, 'docker.log');
|
||||||
|
const fakeDocker = path.join(fakeBin, 'docker');
|
||||||
|
fs.writeFileSync(
|
||||||
|
fakeDocker,
|
||||||
|
`#!/bin/sh
|
||||||
|
printf '%s\\n' "$*" >>"$FAKE_DOCKER_LOG"
|
||||||
|
case "$1:$2" in
|
||||||
|
info:|load:*) exit 0 ;;
|
||||||
|
image:inspect)
|
||||||
|
case "$*" in
|
||||||
|
*local-application*) printf '%s\\n' 'sha256:${'1'.repeat(
|
||||||
|
64,
|
||||||
|
)}|arm64|65532:65532|${revision}' ;;
|
||||||
|
*local-operator*) printf '%s\\n' 'sha256:${'2'.repeat(
|
||||||
|
64,
|
||||||
|
)}|arm64|65532:65532|${revision}|short-lived|none-by-default' ;;
|
||||||
|
*) exit 1 ;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
logs:*) printf '%s\\n' '{"event":"active"}'; exit 0 ;;
|
||||||
|
inspect:*) printf '%s\\n' 'true'; exit 0 ;;
|
||||||
|
esac
|
||||||
|
case " $* " in
|
||||||
|
*' --detach '*) printf '%s\\n' 'fake-container-id'; exit 0 ;;
|
||||||
|
*'/setup.json'*) printf '%s\\n' '{"status":"prepared"}'; exit 0 ;;
|
||||||
|
*'/owner-provision.json'*) printf '%s\\n' '{"status":"inserted"}'; exit 0 ;;
|
||||||
|
*'/owner-challenge.json'*) printf '%s\\n' '{"status":"inserted"}'; exit 0 ;;
|
||||||
|
*'/owner-claim.json'*) printf '%s\\n' '{"status":"inserted","role":"owner"}'; exit 0 ;;
|
||||||
|
esac
|
||||||
|
exit 1
|
||||||
|
`,
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
|
const dataRoot = path.join(paths.fixtureRoot, 'quickstart-data');
|
||||||
|
const run = spawnSync(
|
||||||
|
'sh',
|
||||||
|
[
|
||||||
|
path.join(paths.outputRoot, 'quickstart.sh'),
|
||||||
|
'edge',
|
||||||
|
dataRoot,
|
||||||
|
'ql3-alpha-test',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
encoding: 'utf8',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
FAKE_DOCKER_LOG: dockerLog,
|
||||||
|
PATH: `${fakeBin}:${process.env.PATH}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
run.status,
|
||||||
|
0,
|
||||||
|
`${run.stderr}\n${run.stdout}\n${fs.readFileSync(dockerLog, 'utf8')}`,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
run.stdout,
|
||||||
|
/QingLong 3\.0 Local Alpha is active \(edge, arm64\)/,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(path.join(dataRoot, 'container.id'), 'utf8'),
|
||||||
|
'fake-container-id\n',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.join(dataRoot, 'results', 'owner-claim.json.result.json'),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
).role,
|
||||||
|
'owner',
|
||||||
|
);
|
||||||
|
const calls = fs.readFileSync(dockerLog, 'utf8');
|
||||||
|
assert.match(
|
||||||
|
calls,
|
||||||
|
/load --input .*qinglong3-local-trial-kit-arm64\.docker\.tar/,
|
||||||
|
);
|
||||||
|
assert.match(calls, /--memory 128m --memory-swap 128m/);
|
||||||
|
assert.match(calls, /--pids-limit 64/);
|
||||||
|
assert.doesNotMatch(calls, /--network (?!none)/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create rejects an image reference that could alter the shell journey', (t) => {
|
||||||
|
const paths = fixture(t);
|
||||||
|
const options = createOptions(paths);
|
||||||
|
options.applicationImage = 'qinglong3-local-application:test;unexpected';
|
||||||
|
assert.throws(
|
||||||
|
() => createLocalAlphaTrialKit(options, adapters()),
|
||||||
|
/application image reference is invalid/,
|
||||||
|
);
|
||||||
|
assert.equal(fs.existsSync(paths.outputRoot), false);
|
||||||
|
});
|
||||||
|
|
||||||
test('create rejects verification detached from the reviewed workflow', (t) => {
|
test('create rejects verification detached from the reviewed workflow', (t) => {
|
||||||
const paths = fixture(t);
|
const paths = fixture(t);
|
||||||
const evidence = JSON.parse(
|
const evidence = JSON.parse(
|
||||||
|
|||||||
Reference in New Issue
Block a user