mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): add generic worker management entry
This commit is contained in:
@@ -11,6 +11,9 @@
|
|||||||
"configFile": "/secure/qinglong3/plugin-package-client.json",
|
"configFile": "/secure/qinglong3/plugin-package-client.json",
|
||||||
"kubernetesFile": "/secure/qinglong3/plugin-package-kubernetes.json"
|
"kubernetesFile": "/secure/qinglong3/plugin-package-kubernetes.json"
|
||||||
},
|
},
|
||||||
|
"worker": {
|
||||||
|
"configFile": "/secure/qinglong3/worker-client.json"
|
||||||
|
},
|
||||||
"worker-credential": {
|
"worker-credential": {
|
||||||
"configFile": "/secure/qinglong3/worker-credential-client.json"
|
"configFile": "/secure/qinglong3/worker-credential-client.json"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,25 @@
|
|||||||
|
|
||||||
最新增量证据(2026-08-20):
|
最新增量证据(2026-08-20):
|
||||||
|
|
||||||
|
- D-374/ADR-0467(已接受):完成 D-373 留下的 Worker 产品命名债务,同时保持零新增常驻服务。通用 canonical path 为
|
||||||
|
`/api/v3/workers/management`;旧 `/api/v3/worker-credentials/management` 作为精确兼容 alias 继续由同一个 TLS 1.3/mTLS/
|
||||||
|
OIDC listener、transport、quota、rate limiter 和连接集合处理。共享 host 只允许这一对 alias,任意跨 Run/Automation/
|
||||||
|
Approval/Package 管理面的路径组合在监听前失败。新增 `ql3-worker-client` 与 `ql3-cluster-admin worker inspect|list`,只接受
|
||||||
|
caller-driven point inspect 或固定 16 项 keyset page,不接受 command file、credential mutation、caller limit、filter、自动
|
||||||
|
翻页、重试、轮询或 cache;输出固定为 `qinglong/worker-session-inspection@v1` 或
|
||||||
|
`qinglong/worker-session-list@v1`,不投影 transport request/inspection identity。旧 `worker-credential` binary、export、配置和
|
||||||
|
Kubernetes ceremony 保持兼容。实现仍在现有 `@qinglong/cluster-admin` 的内聚 `worker-management/` 目录,不新增 workspace
|
||||||
|
package、dependency、端口、Deployment、数据库对象或权限。聚焦实现门 `87/87`;Cluster Admin 全量为
|
||||||
|
`427 total / 424 pass / 3 conditional skip / 0 fail`,完整 backend 为
|
||||||
|
`1,503 total / 1,501 pass / 2 conditional skip / 0 fail`(包含一条不进入本阶段提交的既有用户测试),18-package clean
|
||||||
|
build/逐包测试单次退出 0。package boundary、Cluster dependency、Edge import、Cluster deployment 与 Worker deployment
|
||||||
|
审计全部 compatible;workspace 仍为 18 packages、`singleSourcePackages=[]`、`shallowSourcePackages=[]`,Cluster Admin 为
|
||||||
|
`128 source / 127 nested`。14 档 Local artifact audit 全部 compatible;基础 Edge/Standalone 为
|
||||||
|
`2,598,669 / 2,598,747` bytes、57 loaded modules,Application+AI 为 `4,501,822 / 4,501,954` bytes,MCP 为
|
||||||
|
`7,324,601 / 7,324,709` bytes、227 loaded modules。本切片不改变 PostgreSQL schema、ACL、repository、role、Pool、连接或
|
||||||
|
failover 语义,因此不重跑和不重新占有物理 HA 证明;D-373 的 PostgreSQL 18.6 arm64 HA `146/146`、timeline `1→2`
|
||||||
|
仅作为相邻既有基线,后续任何数据库语义变化必须重新执行 HA 门。
|
||||||
|
|
||||||
- D-373/ADR-0466(已接受):完成 D-372 的首个 caller-driven Worker 管理只读面。在既有 Worker management
|
- D-373/ADR-0466(已接受):完成 D-372 的首个 caller-driven Worker 管理只读面。在既有 Worker management
|
||||||
service/transport/client 内增加 `worker-session.inspect|list`,继续复用同一个 TLS 1.3/mTLS/OIDC listener、强 User
|
service/transport/client 内增加 `worker-session.inspect|list`,继续复用同一个 TLS 1.3/mTLS/OIDC listener、强 User
|
||||||
`worker.manage` 与耐久 `worker-session.observe` quota,不新增 package、服务、端口、连接池、timer、watcher、queue、
|
`worker.manage` 与耐久 `worker-session.observe` quota,不新增 package、服务、端口、连接池、timer、watcher、queue、
|
||||||
@@ -6480,8 +6499,9 @@ Maintainers 要求 ARMv6、ARMv7 或 386 成为 3.0 正式支持项,必须先
|
|||||||
D-373/ADR-0466 已把该分层投影到 caller-driven Worker 管理只读面:operator 可以 point inspect 或显式读取固定 16 项
|
D-373/ADR-0466 已把该分层投影到 caller-driven Worker 管理只读面:operator 可以 point inspect 或显式读取固定 16 项
|
||||||
keyset page,区分 Session lifecycle 与 Placement compatibility,并查看有界 runtime/capacity;不能请求任意 filter、
|
keyset page,区分 Session lifecycle 与 Placement compatibility,并查看有界 runtime/capacity;不能请求任意 filter、
|
||||||
limit、自动翻页或轮询。该能力复用现有 Worker manager 进程和 `worker.manage`/耐久 quota,PostgreSQL role 只新增
|
limit、自动翻页或轮询。该能力复用现有 Worker manager 进程和 `worker.manage`/耐久 quota,PostgreSQL role 只新增
|
||||||
`SELECT worker_sessions`,所以不会进入 Edge/Standalone 常驻闭包。alpha 的 credential-named URI/CLI 是待 Beta 迁移的
|
`SELECT worker_sessions`,所以不会进入 Edge/Standalone 常驻闭包。D-374/ADR-0467 已用 canonical
|
||||||
命名债务,不得用新建第二套 listener、Deployment 或 package 的方式修复。
|
`/api/v3/workers/management`、只读 `ql3-worker-client` 和同 listener 的精确 legacy alias 清偿 credential-named URI/CLI
|
||||||
|
债务;旧 credential ceremony 继续兼容,且没有新建第二套 listener、Deployment 或 package。
|
||||||
|
|
||||||
### 7.10 资源基准与发布门禁
|
### 7.10 资源基准与发布门禁
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# ADR-0467:通用 Worker Management 产品入口与兼容路径
|
||||||
|
|
||||||
|
- 状态:Accepted
|
||||||
|
- 日期:2026-08-20
|
||||||
|
- 关联 RFC:QL-RFC-0001 D-374、D-14、D-16、D-107
|
||||||
|
- 关联 ADR:ADR-0059、ADR-0465、ADR-0466
|
||||||
|
- Amends:ADR-0466 的 Beta 前命名债务与产品入口
|
||||||
|
|
||||||
|
## 上下文
|
||||||
|
|
||||||
|
ADR-0466 已提供 `worker-session.inspect|list`,但 alpha 期间它仍挂在
|
||||||
|
`/api/v3/worker-credentials/management` 与 `ql3-worker-credential-client` 下。这个入口可以验证底层权限、配额、数据库
|
||||||
|
投影和响应边界,却不是清晰的通用 Worker 产品面:只想判断 Worker 在线、兼容性或剩余 slot 的 operator 不应接触可提交
|
||||||
|
credential mutation command file 的客户端。
|
||||||
|
|
||||||
|
直接新增服务、listener、Deployment 或 workspace package 会复制 TLS/OIDC、连接池、quota 和运维生命周期,也会让低资源
|
||||||
|
路由设备与集群控制节点承担空闲开销。直接把旧入口改名则会破坏已经部署的 credential client、Job 和配置文件。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
1. `/api/v3/workers/management` 成为通用 Worker management canonical path;历史
|
||||||
|
`/api/v3/worker-credentials/management` 保留为精确兼容 alias。两个路径由同一个 HTTPS server、端口、transport、身份、
|
||||||
|
rate limiter、并发计数和连接集合处理,不创建第二个 listener 或进程。
|
||||||
|
2. 共享 HTTP host 不接受任意 alias。只有 canonical Worker path 可以同时声明且只能声明一个 legacy credential path;把 Run、
|
||||||
|
Automation、Approval、Package 或未知路径拼入 alias 列表必须在监听前失败。
|
||||||
|
3. 新增 `worker` 管理客户端策略,固定 canonical path 和 required mTLS;既有 `worker-credential` 策略继续固定 legacy path。
|
||||||
|
旧 binary、export、Kubernetes Job 与配置保持可用,不要求原子迁移。
|
||||||
|
4. `ql3-worker-client` 与 `ql3-cluster-admin worker` 只提供:
|
||||||
|
- `inspect --project=PROJECT --worker=WORKER`;
|
||||||
|
- `list --project=PROJECT [--after=WORKER]`。
|
||||||
|
客户端不接受 command file、credential operation、caller limit、filter、自动翻页、重试、轮询、cache 或后台刷新。每次调用
|
||||||
|
只产生一个内部 inspection ID 和一次 POST。
|
||||||
|
5. 产品输出使用 `qinglong/worker-session-inspection@v1` 与 `qinglong/worker-session-list@v1` 固定 schema。它复用 ADR-0466
|
||||||
|
的严格 transport validator,再执行独立投影;不返回 HTTP request ID、inspection ID、assertion、credential、Secret、
|
||||||
|
raw capability 或可扩展任意字段。文本卡片与 JSON 均由同一投影生成。
|
||||||
|
6. generic client 与 credential-compatible client 位于现有 `@qinglong/cluster-admin` 内。`worker-management/` 以 client、product、
|
||||||
|
CLI 三个职责形成内聚目录,不新增 workspace package,也不把单文件边界放进 `packages/`。
|
||||||
|
7. operator context 可以同时声明 `worker` 和 `worker-credential` 配置。前者用于只读日常观察,后者只在显式凭据管理 ceremony
|
||||||
|
中使用;context 仍只持有 owner-private 配置路径,assertion 每次调用显式提供。
|
||||||
|
8. 该切片不修改 PostgreSQL schema、role、Session、Scheduler 或 Worker ingress。Edge/Standalone 与未启用 Cluster Worker
|
||||||
|
manager 的部署不会加载 Cluster Admin 客户端;小设备没有新 timer、socket、数据库连接、常驻模块或磁盘写入。
|
||||||
|
|
||||||
|
## 升级与回滚
|
||||||
|
|
||||||
|
- 先发布同时接受 canonical/legacy path 的 manager,再分发 generic client 配置;旧客户端可以在整个兼容窗口继续工作。
|
||||||
|
- generic 配置必须精确指向 `/api/v3/workers/management`,legacy credential 配置不能被 generic client 接受。这样可防止一次配置
|
||||||
|
漂移重新暴露 mutation surface。
|
||||||
|
- 回滚客户端不会影响 manager;回滚 manager 到 ADR-0466 时 generic path 暂不可用,但 legacy credential path 与数据库语义保持。
|
||||||
|
因本切片没有 schema/ACL 变化,不需要数据库降级或 HA promotion。
|
||||||
|
|
||||||
|
## 被拒绝的替代方案
|
||||||
|
|
||||||
|
### 新建 Worker observability 服务或 package
|
||||||
|
|
||||||
|
拒绝。它会复制安全边界、增加镜像/部署/连接池和路由设备供应链成本,而底层只有两个 caller-driven read operation。
|
||||||
|
|
||||||
|
### 直接重命名旧 path 与 binary
|
||||||
|
|
||||||
|
拒绝。已部署 Job、config 和 operator automation 会被无收益破坏。canonical-first 加精确 alias 能提供可迁移产品入口,同时保持
|
||||||
|
旧 ceremony 的明确语义。
|
||||||
|
|
||||||
|
### 让新 CLI 继续接受任意 command file
|
||||||
|
|
||||||
|
拒绝。即使 TypeScript 类型写成只读,JavaScript caller 仍可构造 credential mutation;generic boundary 必须在 runtime
|
||||||
|
normalizer 和 CLI parser 两层拒绝该词汇。
|
||||||
|
|
||||||
|
### 自动轮询或自动翻完所有 Worker
|
||||||
|
|
||||||
|
拒绝。它会把一次低成本诊断变成不可预测数据库和网络负载。固定 16 项页面与显式 `--after` 让低配 manager 和 operator 都能
|
||||||
|
控制每次成本。
|
||||||
|
|
||||||
|
## 验证与证据
|
||||||
|
|
||||||
|
- 聚焦实现门 `87/87`,覆盖 canonical/legacy 同 listener、mTLS/CRL、cross-plane alias 拒绝、通用策略、只读 normalizer、严格
|
||||||
|
产品投影、CLI 真进程单请求、mutation 词汇拒绝、产品 catalog/context、部署审计和 package 内部布局。
|
||||||
|
- `@qinglong/cluster-admin` TypeScript build/check 通过;全量为
|
||||||
|
`427 total / 424 pass / 3 conditional skip / 0 fail`。完整 backend 为
|
||||||
|
`1,503 total / 1,501 pass / 2 conditional skip / 0 fail`(包含一条不进入本阶段提交的既有用户测试)。
|
||||||
|
- 18-package clean build/逐包测试单次退出 0;package boundary、Cluster dependency、Edge import、Cluster deployment 与
|
||||||
|
Worker deployment 审计全部 compatible。workspace 保持 18 packages、`singleSourcePackages=[]`、
|
||||||
|
`shallowSourcePackages=[]`;Cluster Admin 为 `128 source / 127 nested`,没有新增外部 dependency。
|
||||||
|
- 14 档 Local artifact audit 全部 compatible;基础 Edge/Standalone 为 `2,598,669 / 2,598,747` bytes、57 loaded modules,
|
||||||
|
Application+AI 为 `4,501,822 / 4,501,954` bytes,MCP 为 `7,324,601 / 7,324,709` bytes、227 loaded modules,证明
|
||||||
|
generic Worker 客户端没有进入低配设备常驻闭包。
|
||||||
|
- 本切片不触及 PostgreSQL schema、ACL、repository、role、Pool、连接或 failover 语义,因此不重跑和不重新占有物理 HA
|
||||||
|
证明;复用 ADR-0466 的 PostgreSQL 18.6 arm64 HA `146/146`、timeline `1→2` 相邻基线。后续若改变数据库语义必须重新运行
|
||||||
|
HA,不能沿用该豁免。
|
||||||
|
|
||||||
|
## 后续边界
|
||||||
|
|
||||||
|
- legacy credential path/binary 的移除必须另立版本化弃用 ADR、发布遥测与至少一个兼容窗口;本 ADR 不授权删除。
|
||||||
|
- Console/UI 若接入只能显式点击读取,不得静默轮询、自动翻页或把 assertion 存入浏览器持久存储。
|
||||||
|
- Worker 历史、指标、label/filter 或跨 Project inventory 仍需独立 ownership、索引、retention、隐私和资源预算决策。
|
||||||
@@ -470,6 +470,7 @@
|
|||||||
| [ADR-0464](./ADR-0464-machine-enforced-node24-architecture-support-tiers.md) | 机器化 Node 24 架构支持分层 | Accepted |
|
| [ADR-0464](./ADR-0464-machine-enforced-node24-architecture-support-tiers.md) | 机器化 Node 24 架构支持分层 | Accepted |
|
||||||
| [ADR-0465](./ADR-0465-versioned-worker-support-tier-admission.md) | 版本化 Worker 支持等级准入 | Accepted |
|
| [ADR-0465](./ADR-0465-versioned-worker-support-tier-admission.md) | 版本化 Worker 支持等级准入 | Accepted |
|
||||||
| [ADR-0466](./ADR-0466-bounded-worker-session-compatibility-observation.md) | 有界 Worker Session 兼容性观察 | Accepted |
|
| [ADR-0466](./ADR-0466-bounded-worker-session-compatibility-observation.md) | 有界 Worker Session 兼容性观察 | Accepted |
|
||||||
|
| [ADR-0467](./ADR-0467-generic-worker-management-product-entry.md) | 通用 Worker Management 产品入口与兼容路径 | Accepted |
|
||||||
|
|
||||||
## 规则
|
## 规则
|
||||||
|
|
||||||
|
|||||||
@@ -180,6 +180,16 @@
|
|||||||
"require": "./dist/worker-credential/workerCredentialManagementClient.js",
|
"require": "./dist/worker-credential/workerCredentialManagementClient.js",
|
||||||
"default": "./dist/worker-credential/workerCredentialManagementClient.js"
|
"default": "./dist/worker-credential/workerCredentialManagementClient.js"
|
||||||
},
|
},
|
||||||
|
"./worker-management-client": {
|
||||||
|
"types": "./dist/worker-management/workerManagementClient.d.ts",
|
||||||
|
"require": "./dist/worker-management/workerManagementClient.js",
|
||||||
|
"default": "./dist/worker-management/workerManagementClient.js"
|
||||||
|
},
|
||||||
|
"./worker-management-product": {
|
||||||
|
"types": "./dist/worker-management/workerManagementProduct.d.ts",
|
||||||
|
"require": "./dist/worker-management/workerManagementProduct.js",
|
||||||
|
"default": "./dist/worker-management/workerManagementProduct.js"
|
||||||
|
},
|
||||||
"./worker-credential-management-executor": {
|
"./worker-credential-management-executor": {
|
||||||
"types": "./dist/worker-credential/workerCredentialManagementExecutor.d.ts",
|
"types": "./dist/worker-credential/workerCredentialManagementExecutor.d.ts",
|
||||||
"require": "./dist/worker-credential/workerCredentialManagementExecutor.js",
|
"require": "./dist/worker-credential/workerCredentialManagementExecutor.js",
|
||||||
@@ -420,6 +430,7 @@
|
|||||||
"ql3-worker-credential-manage": "dist/worker-credential/management-server/workerCredentialManagementCli.js",
|
"ql3-worker-credential-manage": "dist/worker-credential/management-server/workerCredentialManagementCli.js",
|
||||||
"ql3-worker-credential-execute": "dist/worker-credential/workerCredentialExecutorCli.js",
|
"ql3-worker-credential-execute": "dist/worker-credential/workerCredentialExecutorCli.js",
|
||||||
"ql3-worker-credential-client": "dist/worker-credential/workerCredentialManagementClientCli.js",
|
"ql3-worker-credential-client": "dist/worker-credential/workerCredentialManagementClientCli.js",
|
||||||
|
"ql3-worker-client": "dist/worker-management/workerManagementClientCli.js",
|
||||||
"ql3-approval-manage": "dist/approval-management/approvalManagementCli.js",
|
"ql3-approval-manage": "dist/approval-management/approvalManagementCli.js",
|
||||||
"ql3-approval-client": "dist/approval-management/approvalManagementClientCli.js",
|
"ql3-approval-client": "dist/approval-management/approvalManagementClientCli.js",
|
||||||
"ql3-run-manage": "dist/run-management/runManagementCli.js",
|
"ql3-run-manage": "dist/run-management/runManagementCli.js",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { TextDecoder } from 'node:util';
|
|||||||
|
|
||||||
export type ClusterAuthenticatedManagementClientKind =
|
export type ClusterAuthenticatedManagementClientKind =
|
||||||
| 'package'
|
| 'package'
|
||||||
|
| 'worker'
|
||||||
| 'worker-credential'
|
| 'worker-credential'
|
||||||
| 'automation'
|
| 'automation'
|
||||||
| 'approval'
|
| 'approval'
|
||||||
@@ -34,6 +35,10 @@ const MANAGEMENT_CLIENT_POLICIES: Readonly<
|
|||||||
managementPath: '/api/v3/plugin-packages/management',
|
managementPath: '/api/v3/plugin-packages/management',
|
||||||
clientCertificate: 'forbidden',
|
clientCertificate: 'forbidden',
|
||||||
}),
|
}),
|
||||||
|
worker: Object.freeze({
|
||||||
|
managementPath: '/api/v3/workers/management',
|
||||||
|
clientCertificate: 'required',
|
||||||
|
}),
|
||||||
'worker-credential': Object.freeze({
|
'worker-credential': Object.freeze({
|
||||||
managementPath: '/api/v3/worker-credentials/management',
|
managementPath: '/api/v3/worker-credentials/management',
|
||||||
clientCertificate: 'required',
|
clientCertificate: 'required',
|
||||||
@@ -147,7 +152,9 @@ export function readCanonicalFile(
|
|||||||
throw configurationFailure();
|
throw configurationFailure();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ClusterPluginPackageManagementClientConfigurationError) {
|
if (
|
||||||
|
error instanceof ClusterPluginPackageManagementClientConfigurationError
|
||||||
|
) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw configurationFailure();
|
throw configurationFailure();
|
||||||
@@ -209,7 +216,9 @@ export function readCanonicalFile(
|
|||||||
return bytes;
|
return bytes;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
bytes?.fill(0);
|
bytes?.fill(0);
|
||||||
if (error instanceof ClusterPluginPackageManagementClientConfigurationError) {
|
if (
|
||||||
|
error instanceof ClusterPluginPackageManagementClientConfigurationError
|
||||||
|
) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw configurationFailure();
|
throw configurationFailure();
|
||||||
@@ -349,7 +358,10 @@ export function prepareClusterAuthenticatedManagementClientConfiguration(
|
|||||||
throw configurationFailure();
|
throw configurationFailure();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ClusterPluginPackageManagementClientConfigurationError) {
|
if (
|
||||||
|
error instanceof
|
||||||
|
ClusterPluginPackageManagementClientConfigurationError
|
||||||
|
) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw configurationFailure();
|
throw configurationFailure();
|
||||||
@@ -380,7 +392,9 @@ export function prepareClusterAuthenticatedManagementClientConfiguration(
|
|||||||
caBytes?.fill(0);
|
caBytes?.fill(0);
|
||||||
clientCertificateBytes?.fill(0);
|
clientCertificateBytes?.fill(0);
|
||||||
clientPrivateKeyBytes?.fill(0);
|
clientPrivateKeyBytes?.fill(0);
|
||||||
if (error instanceof ClusterPluginPackageManagementClientConfigurationError) {
|
if (
|
||||||
|
error instanceof ClusterPluginPackageManagementClientConfigurationError
|
||||||
|
) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw configurationFailure();
|
throw configurationFailure();
|
||||||
@@ -408,11 +422,10 @@ export function validateClusterAuthenticatedManagementClientConfiguration(
|
|||||||
): Readonly<ClusterAuthenticatedManagementClientConfigurationSummary> {
|
): Readonly<ClusterAuthenticatedManagementClientConfigurationSummary> {
|
||||||
const policy = MANAGEMENT_CLIENT_POLICIES[kind];
|
const policy = MANAGEMENT_CLIENT_POLICIES[kind];
|
||||||
if (policy === undefined) throw configurationFailure();
|
if (policy === undefined) throw configurationFailure();
|
||||||
const prepared =
|
const prepared = prepareClusterAuthenticatedManagementClientKindConfiguration(
|
||||||
prepareClusterAuthenticatedManagementClientKindConfiguration(
|
configFile,
|
||||||
configFile,
|
kind,
|
||||||
kind,
|
);
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export const CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_PATH =
|
|||||||
'/api/v3/plugin-packages/management';
|
'/api/v3/plugin-packages/management';
|
||||||
export const CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH =
|
export const CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH =
|
||||||
'/api/v3/worker-credentials/management';
|
'/api/v3/worker-credentials/management';
|
||||||
|
export const CLUSTER_WORKER_MANAGEMENT_PATH = '/api/v3/workers/management';
|
||||||
export const CLUSTER_AUTOMATION_MANAGEMENT_PATH =
|
export const CLUSTER_AUTOMATION_MANAGEMENT_PATH =
|
||||||
'/api/v3/automations/management';
|
'/api/v3/automations/management';
|
||||||
export const CLUSTER_APPROVAL_MANAGEMENT_PATH = '/api/v3/approvals/management';
|
export const CLUSTER_APPROVAL_MANAGEMENT_PATH = '/api/v3/approvals/management';
|
||||||
@@ -94,6 +95,7 @@ export const CLUSTER_RUN_MANAGEMENT_PATH = '/api/v3/runs/management';
|
|||||||
export type ClusterAuthenticatedManagementPath =
|
export type ClusterAuthenticatedManagementPath =
|
||||||
| typeof CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_PATH
|
| typeof CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_PATH
|
||||||
| typeof CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH
|
| typeof CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH
|
||||||
|
| typeof CLUSTER_WORKER_MANAGEMENT_PATH
|
||||||
| typeof CLUSTER_AUTOMATION_MANAGEMENT_PATH
|
| typeof CLUSTER_AUTOMATION_MANAGEMENT_PATH
|
||||||
| typeof CLUSTER_APPROVAL_MANAGEMENT_PATH
|
| typeof CLUSTER_APPROVAL_MANAGEMENT_PATH
|
||||||
| typeof CLUSTER_MODEL_PROVIDER_CREDENTIAL_MANAGEMENT_PATH
|
| typeof CLUSTER_MODEL_PROVIDER_CREDENTIAL_MANAGEMENT_PATH
|
||||||
@@ -101,6 +103,7 @@ export type ClusterAuthenticatedManagementPath =
|
|||||||
const MANAGEMENT_PATHS = new Set<ClusterAuthenticatedManagementPath>([
|
const MANAGEMENT_PATHS = new Set<ClusterAuthenticatedManagementPath>([
|
||||||
CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_PATH,
|
CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_PATH,
|
||||||
CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH,
|
CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH,
|
||||||
|
CLUSTER_WORKER_MANAGEMENT_PATH,
|
||||||
CLUSTER_AUTOMATION_MANAGEMENT_PATH,
|
CLUSTER_AUTOMATION_MANAGEMENT_PATH,
|
||||||
CLUSTER_APPROVAL_MANAGEMENT_PATH,
|
CLUSTER_APPROVAL_MANAGEMENT_PATH,
|
||||||
CLUSTER_MODEL_PROVIDER_CREDENTIAL_MANAGEMENT_PATH,
|
CLUSTER_MODEL_PROVIDER_CREDENTIAL_MANAGEMENT_PATH,
|
||||||
@@ -143,6 +146,7 @@ export interface StartClusterPluginPackageManagementHttpOptions {
|
|||||||
readonly transport: ClusterAuthenticatedManagementTransport;
|
readonly transport: ClusterAuthenticatedManagementTransport;
|
||||||
readonly identities: ClusterPluginPackageIdentityKeysetFile;
|
readonly identities: ClusterPluginPackageIdentityKeysetFile;
|
||||||
readonly managementPath?: ClusterAuthenticatedManagementPath;
|
readonly managementPath?: ClusterAuthenticatedManagementPath;
|
||||||
|
readonly compatibleManagementPaths?: readonly ClusterAuthenticatedManagementPath[];
|
||||||
readonly limits?: ClusterPluginPackageManagementHttpLimits;
|
readonly limits?: ClusterPluginPackageManagementHttpLimits;
|
||||||
readonly now?: () => number;
|
readonly now?: () => number;
|
||||||
readonly createRequestId?: () => string;
|
readonly createRequestId?: () => string;
|
||||||
@@ -547,7 +551,8 @@ function responseError(error: unknown): HttpRequestError {
|
|||||||
error instanceof ClusterApprovalManagementTransportAuthenticationError ||
|
error instanceof ClusterApprovalManagementTransportAuthenticationError ||
|
||||||
error instanceof
|
error instanceof
|
||||||
ClusterModelProviderCredentialManagementTransportAuthenticationError ||
|
ClusterModelProviderCredentialManagementTransportAuthenticationError ||
|
||||||
error instanceof ClusterModelProviderCredentialManagementAuthenticationError ||
|
error instanceof
|
||||||
|
ClusterModelProviderCredentialManagementAuthenticationError ||
|
||||||
error instanceof ClusterRunManagementTransportAuthenticationError
|
error instanceof ClusterRunManagementTransportAuthenticationError
|
||||||
) {
|
) {
|
||||||
return new HttpRequestError(401, 'authentication_required');
|
return new HttpRequestError(401, 'authentication_required');
|
||||||
@@ -573,7 +578,8 @@ function responseError(error: unknown): HttpRequestError {
|
|||||||
error instanceof WorkerCredentialManagementAuthorizationError ||
|
error instanceof WorkerCredentialManagementAuthorizationError ||
|
||||||
error instanceof ClusterAutomationManagementAuthorizationError ||
|
error instanceof ClusterAutomationManagementAuthorizationError ||
|
||||||
error instanceof ClusterApprovalManagementTransportAuthorizationError ||
|
error instanceof ClusterApprovalManagementTransportAuthorizationError ||
|
||||||
error instanceof ClusterModelProviderCredentialManagementAuthorizationError ||
|
error instanceof
|
||||||
|
ClusterModelProviderCredentialManagementAuthorizationError ||
|
||||||
error instanceof ClusterRunManagementAuthorizationError
|
error instanceof ClusterRunManagementAuthorizationError
|
||||||
) {
|
) {
|
||||||
return new HttpRequestError(403, 'forbidden');
|
return new HttpRequestError(403, 'forbidden');
|
||||||
@@ -665,6 +671,7 @@ export async function startClusterPluginPackageManagementHttp(
|
|||||||
'transport',
|
'transport',
|
||||||
'identities',
|
'identities',
|
||||||
'managementPath',
|
'managementPath',
|
||||||
|
'compatibleManagementPaths',
|
||||||
'limits',
|
'limits',
|
||||||
'now',
|
'now',
|
||||||
'createRequestId',
|
'createRequestId',
|
||||||
@@ -711,6 +718,12 @@ export async function startClusterPluginPackageManagementHttp(
|
|||||||
typeof options.identities.reload !== 'function' ||
|
typeof options.identities.reload !== 'function' ||
|
||||||
(options.managementPath !== undefined &&
|
(options.managementPath !== undefined &&
|
||||||
!MANAGEMENT_PATHS.has(options.managementPath)) ||
|
!MANAGEMENT_PATHS.has(options.managementPath)) ||
|
||||||
|
(options.compatibleManagementPaths !== undefined &&
|
||||||
|
(!Array.isArray(options.compatibleManagementPaths) ||
|
||||||
|
options.managementPath !== CLUSTER_WORKER_MANAGEMENT_PATH ||
|
||||||
|
options.compatibleManagementPaths.length !== 1 ||
|
||||||
|
options.compatibleManagementPaths[0] !==
|
||||||
|
CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH)) ||
|
||||||
(options.now !== undefined && typeof options.now !== 'function') ||
|
(options.now !== undefined && typeof options.now !== 'function') ||
|
||||||
(options.createRequestId !== undefined &&
|
(options.createRequestId !== undefined &&
|
||||||
typeof options.createRequestId !== 'function') ||
|
typeof options.createRequestId !== 'function') ||
|
||||||
@@ -721,6 +734,10 @@ export async function startClusterPluginPackageManagementHttp(
|
|||||||
const limits = reviewedLimits(options.limits);
|
const limits = reviewedLimits(options.limits);
|
||||||
const managementPath =
|
const managementPath =
|
||||||
options.managementPath ?? CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_PATH;
|
options.managementPath ?? CLUSTER_PLUGIN_PACKAGE_MANAGEMENT_PATH;
|
||||||
|
const managementPaths = new Set<ClusterAuthenticatedManagementPath>([
|
||||||
|
managementPath,
|
||||||
|
...(options.compatibleManagementPaths ?? []),
|
||||||
|
]);
|
||||||
const now = options.now ?? Date.now;
|
const now = options.now ?? Date.now;
|
||||||
const createRequestId = options.createRequestId ?? randomUUID;
|
const createRequestId = options.createRequestId ?? randomUUID;
|
||||||
const clientCertificateRequired =
|
const clientCertificateRequired =
|
||||||
@@ -824,7 +841,10 @@ export async function startClusterPluginPackageManagementHttp(
|
|||||||
) {
|
) {
|
||||||
throw new HttpRequestError(401, 'client_certificate_required');
|
throw new HttpRequestError(401, 'client_certificate_required');
|
||||||
}
|
}
|
||||||
if (request.method !== 'POST' || url !== managementPath) {
|
if (
|
||||||
|
request.method !== 'POST' ||
|
||||||
|
!managementPaths.has(url as ClusterAuthenticatedManagementPath)
|
||||||
|
) {
|
||||||
throw new HttpRequestError(404, 'not_found');
|
throw new HttpRequestError(404, 'not_found');
|
||||||
}
|
}
|
||||||
if (availability !== 'ready') {
|
if (availability !== 'ready') {
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ export const QINGLONG3_CLUSTER_PRODUCT_COMMANDS: readonly QingLong3ClusterProduc
|
|||||||
'plugin-package/management/pluginPackageManagementKubernetesClientCli.js',
|
'plugin-package/management/pluginPackageManagementKubernetesClientCli.js',
|
||||||
description: 'manage Plugin Packages through a bounded Kubernetes tunnel',
|
description: 'manage Plugin Packages through a bounded Kubernetes tunnel',
|
||||||
}),
|
}),
|
||||||
|
Object.freeze({
|
||||||
|
name: 'worker',
|
||||||
|
binary: 'ql3-worker-client',
|
||||||
|
target: 'worker-management/workerManagementClientCli.js',
|
||||||
|
description: 'inspect bounded Worker session state',
|
||||||
|
}),
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
name: 'worker-credential',
|
name: 'worker-credential',
|
||||||
binary: 'ql3-worker-credential-client',
|
binary: 'ql3-worker-credential-client',
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ import {
|
|||||||
probeClusterCopilotClientReadiness,
|
probeClusterCopilotClientReadiness,
|
||||||
validateClusterCopilotClientConfiguration,
|
validateClusterCopilotClientConfiguration,
|
||||||
} from '../copilot-client/client';
|
} from '../copilot-client/client';
|
||||||
import {
|
import { validateClusterAuthenticatedManagementClientConfiguration } from '../management-support/pluginPackageManagementClient';
|
||||||
validateClusterAuthenticatedManagementClientConfiguration,
|
|
||||||
} from '../management-support/pluginPackageManagementClient';
|
|
||||||
import type { ClusterAuthenticatedManagementClientKind } from '../management-support/pluginPackageManagementClient';
|
import type { ClusterAuthenticatedManagementClientKind } from '../management-support/pluginPackageManagementClient';
|
||||||
import { probeClusterAuthenticatedManagementClientReadiness } from '../management-support/managementReadinessProbe';
|
import { probeClusterAuthenticatedManagementClientReadiness } from '../management-support/managementReadinessProbe';
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +29,7 @@ const CONTEXT_COMMANDS = Object.freeze([
|
|||||||
'copilot',
|
'copilot',
|
||||||
'package',
|
'package',
|
||||||
'package-kubernetes',
|
'package-kubernetes',
|
||||||
|
'worker',
|
||||||
'worker-credential',
|
'worker-credential',
|
||||||
'approval',
|
'approval',
|
||||||
'run',
|
'run',
|
||||||
@@ -92,6 +91,7 @@ const CONTEXT_COMMAND_CLIENT_KINDS: Readonly<
|
|||||||
> = Object.freeze({
|
> = Object.freeze({
|
||||||
package: 'package',
|
package: 'package',
|
||||||
'package-kubernetes': 'package',
|
'package-kubernetes': 'package',
|
||||||
|
worker: 'worker',
|
||||||
'worker-credential': 'worker-credential',
|
'worker-credential': 'worker-credential',
|
||||||
approval: 'approval',
|
approval: 'approval',
|
||||||
run: 'run',
|
run: 'run',
|
||||||
@@ -337,11 +337,10 @@ export async function validateQingLong3ClusterProductContext(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else if (name === 'package-kubernetes') {
|
} else if (name === 'package-kubernetes') {
|
||||||
const https =
|
const https = validateClusterAuthenticatedManagementClientConfiguration(
|
||||||
validateClusterAuthenticatedManagementClientConfiguration(
|
command.configFile,
|
||||||
command.configFile,
|
CONTEXT_COMMAND_CLIENT_KINDS[name],
|
||||||
CONTEXT_COMMAND_CLIENT_KINDS[name],
|
);
|
||||||
);
|
|
||||||
const kubernetes =
|
const kubernetes =
|
||||||
await validateClusterPluginPackageManagementKubernetesConfiguration(
|
await validateClusterPluginPackageManagementKubernetesConfiguration(
|
||||||
command.kubernetesFile!,
|
command.kubernetesFile!,
|
||||||
@@ -355,11 +354,10 @@ export async function validateQingLong3ClusterProductContext(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const https =
|
const https = validateClusterAuthenticatedManagementClientConfiguration(
|
||||||
validateClusterAuthenticatedManagementClientConfiguration(
|
command.configFile,
|
||||||
command.configFile,
|
CONTEXT_COMMAND_CLIENT_KINDS[name],
|
||||||
CONTEXT_COMMAND_CLIENT_KINDS[name],
|
);
|
||||||
);
|
|
||||||
commands.push(
|
commands.push(
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
name,
|
name,
|
||||||
@@ -403,7 +401,7 @@ export async function probeQingLong3ClusterProductContext(
|
|||||||
command.kubernetesFile!,
|
command.kubernetesFile!,
|
||||||
)
|
)
|
||||||
: name === 'copilot'
|
: name === 'copilot'
|
||||||
? await probeClusterCopilotClientReadiness(command.configFile)
|
? await probeClusterCopilotClientReadiness(command.configFile)
|
||||||
: await probeClusterAuthenticatedManagementClientReadiness(
|
: await probeClusterAuthenticatedManagementClientReadiness(
|
||||||
command.configFile,
|
command.configFile,
|
||||||
CONTEXT_COMMAND_CLIENT_KINDS[name],
|
CONTEXT_COMMAND_CLIENT_KINDS[name],
|
||||||
|
|||||||
+7
-4
@@ -1,6 +1,7 @@
|
|||||||
/** TLS 1.3 Worker credential management HTTP adapter boundary. */
|
/** TLS 1.3 Worker credential management HTTP adapter boundary. */
|
||||||
import {
|
import {
|
||||||
CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH,
|
CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH,
|
||||||
|
CLUSTER_WORKER_MANAGEMENT_PATH,
|
||||||
startClusterPluginPackageManagementHttp,
|
startClusterPluginPackageManagementHttp,
|
||||||
type ClusterPluginPackageManagementHttpApplication,
|
type ClusterPluginPackageManagementHttpApplication,
|
||||||
type ClusterPluginPackageManagementHttpLimits,
|
type ClusterPluginPackageManagementHttpLimits,
|
||||||
@@ -32,15 +33,17 @@ export interface StartClusterWorkerCredentialManagementHttpOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the Worker credential management endpoint on the shared Cluster Admin
|
* Starts the Worker management endpoint on the shared Cluster Admin TLS
|
||||||
* TLS 1.3/OIDC boundary. The public manager process never receives credential
|
* 1.3/OIDC boundary. The former credential-scoped path remains an exact
|
||||||
* delivery or Kubernetes execution capabilities.
|
* compatibility alias on this listener. The public manager process never
|
||||||
|
* receives credential delivery or Kubernetes execution capabilities.
|
||||||
*/
|
*/
|
||||||
export async function startClusterWorkerCredentialManagementHttp(
|
export async function startClusterWorkerCredentialManagementHttp(
|
||||||
options: StartClusterWorkerCredentialManagementHttpOptions,
|
options: StartClusterWorkerCredentialManagementHttpOptions,
|
||||||
): Promise<Readonly<ClusterWorkerCredentialManagementHttpApplication>> {
|
): Promise<Readonly<ClusterWorkerCredentialManagementHttpApplication>> {
|
||||||
return startClusterPluginPackageManagementHttp({
|
return startClusterPluginPackageManagementHttp({
|
||||||
...options,
|
...options,
|
||||||
managementPath: CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH,
|
managementPath: CLUSTER_WORKER_MANAGEMENT_PATH,
|
||||||
|
compatibleManagementPaths: [CLUSTER_WORKER_CREDENTIAL_MANAGEMENT_PATH],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/** Read-only product client boundary for bounded Worker session observation. */
|
||||||
|
import {
|
||||||
|
executeClusterAuthenticatedManagementClient,
|
||||||
|
type ClusterAuthenticatedManagementClientResult,
|
||||||
|
type ClusterAuthenticatedManagementCommandExecution,
|
||||||
|
type ClusterPluginPackageManagementClientConnectionOptions,
|
||||||
|
} from '../management-support/pluginPackageManagementClient';
|
||||||
|
import {
|
||||||
|
ClusterWorkerCredentialManagementTransportRequestError,
|
||||||
|
normalizeClusterWorkerCredentialManagementCommand,
|
||||||
|
type ClusterWorkerCredentialManagementCommand,
|
||||||
|
type ClusterWorkerCredentialManagementTransportResult,
|
||||||
|
} from '../worker-credential/management-server/workerCredentialManagementTransport';
|
||||||
|
import { validateClusterWorkerCredentialManagementClientResult } from '../worker-credential/workerCredentialManagementClient';
|
||||||
|
|
||||||
|
const MANAGEMENT_PATH = '/api/v3/workers/management';
|
||||||
|
|
||||||
|
export type ClusterWorkerManagementCommand = Extract<
|
||||||
|
ClusterWorkerCredentialManagementCommand,
|
||||||
|
{ readonly operation: 'worker-session.inspect' | 'worker-session.list' }
|
||||||
|
>;
|
||||||
|
export type ClusterWorkerManagementTransportResult = Extract<
|
||||||
|
ClusterWorkerCredentialManagementTransportResult,
|
||||||
|
{ readonly operation: 'worker-session.inspect' | 'worker-session.list' }
|
||||||
|
>;
|
||||||
|
export type ClusterWorkerManagementClientExecution =
|
||||||
|
ClusterAuthenticatedManagementCommandExecution<ClusterWorkerManagementCommand>;
|
||||||
|
export type ClusterWorkerManagementClientConnectionOptions =
|
||||||
|
ClusterPluginPackageManagementClientConnectionOptions;
|
||||||
|
export type ClusterWorkerManagementClientResult =
|
||||||
|
ClusterAuthenticatedManagementClientResult<ClusterWorkerManagementTransportResult>;
|
||||||
|
|
||||||
|
export function normalizeClusterWorkerManagementCommand(
|
||||||
|
value: unknown,
|
||||||
|
): Readonly<ClusterWorkerManagementCommand> {
|
||||||
|
const command = normalizeClusterWorkerCredentialManagementCommand(value);
|
||||||
|
if (
|
||||||
|
command.operation !== 'worker-session.inspect' &&
|
||||||
|
command.operation !== 'worker-session.list'
|
||||||
|
) {
|
||||||
|
throw new ClusterWorkerCredentialManagementTransportRequestError(
|
||||||
|
'operation is not available through the read-only Worker client',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateClusterWorkerManagementResult(
|
||||||
|
value: unknown,
|
||||||
|
command: Readonly<ClusterWorkerManagementCommand>,
|
||||||
|
): Readonly<ClusterWorkerManagementTransportResult> {
|
||||||
|
const result = validateClusterWorkerCredentialManagementClientResult(
|
||||||
|
value,
|
||||||
|
command,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
result.operation !== 'worker-session.inspect' &&
|
||||||
|
result.operation !== 'worker-session.list'
|
||||||
|
) {
|
||||||
|
throw new Error('Worker management response is invalid');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROTOCOL = Object.freeze({
|
||||||
|
managementPath: MANAGEMENT_PATH,
|
||||||
|
clientCertificate: 'required' as const,
|
||||||
|
normalizeCommand: normalizeClusterWorkerManagementCommand,
|
||||||
|
validateResult: validateClusterWorkerManagementResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function executeClusterWorkerManagementClient(
|
||||||
|
execution: ClusterWorkerManagementClientExecution,
|
||||||
|
connectionOptions?: ClusterWorkerManagementClientConnectionOptions,
|
||||||
|
): Promise<Readonly<ClusterWorkerManagementClientResult>> {
|
||||||
|
return executeClusterAuthenticatedManagementClient(
|
||||||
|
execution,
|
||||||
|
PROTOCOL,
|
||||||
|
connectionOptions,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { ClusterPluginPackageManagementClientRemoteError } from '../management-support/pluginPackageManagementClient';
|
||||||
|
import { executeClusterWorkerManagementClient } from './workerManagementClient';
|
||||||
|
import {
|
||||||
|
createWorkerSessionInspectionCommand,
|
||||||
|
createWorkerSessionListCommand,
|
||||||
|
formatWorkerSessionInspectionCard,
|
||||||
|
formatWorkerSessionListCard,
|
||||||
|
projectWorkerSessionInspection,
|
||||||
|
projectWorkerSessionList,
|
||||||
|
} from './workerManagementProduct';
|
||||||
|
|
||||||
|
const USAGE = [
|
||||||
|
'Usage: ql3-worker-client inspect --config=/absolute/client.json --assertion=/absolute/assertion.jwt --project=PROJECT --worker=WORKER [--format=text|json]',
|
||||||
|
' ql3-worker-client list --config=/absolute/client.json --assertion=/absolute/assertion.jwt --project=PROJECT [--after=WORKER] [--format=text|json]',
|
||||||
|
'',
|
||||||
|
'One invocation performs one bounded read; it never retries, polls or auto-pages.',
|
||||||
|
].join('\n');
|
||||||
|
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||||
|
|
||||||
|
type WorkerClientArguments = Readonly<{
|
||||||
|
kind: 'inspect' | 'list';
|
||||||
|
configFile: string;
|
||||||
|
assertionFile: string;
|
||||||
|
projectId: string;
|
||||||
|
workerId?: string;
|
||||||
|
afterWorkerId?: string;
|
||||||
|
format: 'text' | 'json';
|
||||||
|
}>;
|
||||||
|
|
||||||
|
function argumentsFrom(argv: readonly string[]): WorkerClientArguments | null {
|
||||||
|
const modes = argv.filter(
|
||||||
|
(argument) => argument === 'inspect' || argument === 'list',
|
||||||
|
);
|
||||||
|
if (modes.length !== 1 || argv.length < 4 || argv.length > 6) return null;
|
||||||
|
const kind = modes[0] as 'inspect' | 'list';
|
||||||
|
const values = new Map<string, string>();
|
||||||
|
for (const argument of argv) {
|
||||||
|
if (argument === kind) continue;
|
||||||
|
const match =
|
||||||
|
/^--(config|assertion|project|worker|after|format)=(.+)$/.exec(argument);
|
||||||
|
if (!match || values.has(match[1]!)) return null;
|
||||||
|
values.set(match[1]!, match[2]!);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!values.get('config')?.startsWith('/') ||
|
||||||
|
!values.get('assertion')?.startsWith('/') ||
|
||||||
|
!IDENTIFIER.test(values.get('project') ?? '') ||
|
||||||
|
(kind === 'inspect' &&
|
||||||
|
(!IDENTIFIER.test(values.get('worker') ?? '') || values.has('after'))) ||
|
||||||
|
(kind === 'list' &&
|
||||||
|
(values.has('worker') ||
|
||||||
|
(values.has('after') && !IDENTIFIER.test(values.get('after')!)))) ||
|
||||||
|
(values.has('format') &&
|
||||||
|
values.get('format') !== 'text' &&
|
||||||
|
values.get('format') !== 'json')
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Object.freeze({
|
||||||
|
kind,
|
||||||
|
configFile: values.get('config')!,
|
||||||
|
assertionFile: values.get('assertion')!,
|
||||||
|
projectId: values.get('project')!,
|
||||||
|
...(kind === 'inspect' ? { workerId: values.get('worker')! } : {}),
|
||||||
|
...(kind === 'list' && values.has('after')
|
||||||
|
? { afterWorkerId: values.get('after')! }
|
||||||
|
: {}),
|
||||||
|
format: (values.get('format') ?? 'text') as 'text' | 'json',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function failureFact(error: unknown): Readonly<Record<string, unknown>> {
|
||||||
|
const candidate = error as { readonly code?: unknown };
|
||||||
|
return Object.freeze({
|
||||||
|
schemaVersion: 1,
|
||||||
|
component: 'qinglong3-worker-management-client',
|
||||||
|
event: 'inspection_failed',
|
||||||
|
code:
|
||||||
|
typeof candidate?.code === 'string'
|
||||||
|
? candidate.code
|
||||||
|
: 'QL3_WORKER_MANAGEMENT_CLIENT_FAILED',
|
||||||
|
...(error instanceof ClusterPluginPackageManagementClientRemoteError
|
||||||
|
? {
|
||||||
|
statusCode: error.statusCode,
|
||||||
|
responseCode: error.responseCode,
|
||||||
|
requestId: error.requestId,
|
||||||
|
...(error.retryAfterSeconds === null
|
||||||
|
? {}
|
||||||
|
: { retryAfterSeconds: error.retryAfterSeconds }),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(argv: readonly string[]): Promise<void> {
|
||||||
|
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
|
||||||
|
process.stdout.write(`${USAGE}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const argumentsValue = argumentsFrom(argv);
|
||||||
|
if (argumentsValue === null) {
|
||||||
|
process.stderr.write(
|
||||||
|
`${JSON.stringify({
|
||||||
|
schemaVersion: 1,
|
||||||
|
component: 'qinglong3-worker-management-client',
|
||||||
|
event: 'usage_invalid',
|
||||||
|
code: 'QL3_WORKER_MANAGEMENT_CLIENT_USAGE_INVALID',
|
||||||
|
})}\n`,
|
||||||
|
);
|
||||||
|
process.exitCode = 64;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const command =
|
||||||
|
argumentsValue.kind === 'inspect'
|
||||||
|
? createWorkerSessionInspectionCommand(
|
||||||
|
argumentsValue.projectId,
|
||||||
|
argumentsValue.workerId!,
|
||||||
|
)
|
||||||
|
: createWorkerSessionListCommand(
|
||||||
|
argumentsValue.projectId,
|
||||||
|
argumentsValue.afterWorkerId,
|
||||||
|
);
|
||||||
|
const response = await executeClusterWorkerManagementClient({
|
||||||
|
configFile: argumentsValue.configFile,
|
||||||
|
assertionFile: argumentsValue.assertionFile,
|
||||||
|
command,
|
||||||
|
});
|
||||||
|
const projection =
|
||||||
|
argumentsValue.kind === 'inspect'
|
||||||
|
? projectWorkerSessionInspection(argumentsValue.projectId, response)
|
||||||
|
: projectWorkerSessionList(argumentsValue.projectId, response);
|
||||||
|
process.stdout.write(
|
||||||
|
argumentsValue.format === 'json'
|
||||||
|
? `${JSON.stringify(projection)}\n`
|
||||||
|
: `${
|
||||||
|
projection.schema === 'qinglong/worker-session-inspection@v1'
|
||||||
|
? formatWorkerSessionInspectionCard(projection)
|
||||||
|
: formatWorkerSessionListCard(projection)
|
||||||
|
}\n`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(`${JSON.stringify(failureFact(error))}\n`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void run(process.argv.slice(2));
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/** Bounded commands and low-sensitive product projections for Worker sessions. */
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ClusterWorkerManagementClientResult,
|
||||||
|
ClusterWorkerManagementCommand,
|
||||||
|
ClusterWorkerManagementTransportResult,
|
||||||
|
} from './workerManagementClient';
|
||||||
|
|
||||||
|
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||||
|
|
||||||
|
type InspectCommand = Extract<
|
||||||
|
ClusterWorkerManagementCommand,
|
||||||
|
{ readonly operation: 'worker-session.inspect' }
|
||||||
|
>;
|
||||||
|
type ListCommand = Extract<
|
||||||
|
ClusterWorkerManagementCommand,
|
||||||
|
{ readonly operation: 'worker-session.list' }
|
||||||
|
>;
|
||||||
|
type InspectResult = Extract<
|
||||||
|
ClusterWorkerManagementTransportResult,
|
||||||
|
{ readonly operation: 'worker-session.inspect' }
|
||||||
|
>;
|
||||||
|
type ListResult = Extract<
|
||||||
|
ClusterWorkerManagementTransportResult,
|
||||||
|
{ readonly operation: 'worker-session.list' }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface WorkerSessionInspection {
|
||||||
|
readonly schema: 'qinglong/worker-session-inspection@v1';
|
||||||
|
readonly projectId: string;
|
||||||
|
readonly observedAtMs: number;
|
||||||
|
readonly found: boolean;
|
||||||
|
readonly worker: InspectResult['worker'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkerSessionList {
|
||||||
|
readonly schema: 'qinglong/worker-session-list@v1';
|
||||||
|
readonly projectId: string;
|
||||||
|
readonly observedAtMs: number;
|
||||||
|
readonly count: number;
|
||||||
|
readonly workers: ListResult['workers'];
|
||||||
|
readonly nextAfterWorkerId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClusterWorkerManagementProductError extends TypeError {
|
||||||
|
readonly code = 'QL3_WORKER_MANAGEMENT_PRODUCT_INPUT_INVALID';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('Worker management product input is invalid');
|
||||||
|
this.name = 'ClusterWorkerManagementProductError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function identifier(value: string): string {
|
||||||
|
if (!IDENTIFIER.test(value)) throw new ClusterWorkerManagementProductError();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inspectionId(createId: () => string): string {
|
||||||
|
const value = createId();
|
||||||
|
return identifier(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWorkerSessionInspectionCommand(
|
||||||
|
projectId: string,
|
||||||
|
workerId: string,
|
||||||
|
createId: () => string = randomUUID,
|
||||||
|
): Readonly<InspectCommand> {
|
||||||
|
return Object.freeze({
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'worker-session.inspect',
|
||||||
|
request: Object.freeze({
|
||||||
|
authorityProjectId: identifier(projectId),
|
||||||
|
workerId: identifier(workerId),
|
||||||
|
inspectionId: inspectionId(createId),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWorkerSessionListCommand(
|
||||||
|
projectId: string,
|
||||||
|
afterWorkerId?: string,
|
||||||
|
createId: () => string = randomUUID,
|
||||||
|
): Readonly<ListCommand> {
|
||||||
|
return Object.freeze({
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'worker-session.list',
|
||||||
|
request: Object.freeze({
|
||||||
|
authorityProjectId: identifier(projectId),
|
||||||
|
afterWorkerId:
|
||||||
|
afterWorkerId === undefined ? null : identifier(afterWorkerId),
|
||||||
|
inspectionId: inspectionId(createId),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneWorker<
|
||||||
|
Worker extends
|
||||||
|
| NonNullable<InspectResult['worker']>
|
||||||
|
| ListResult['workers'][number],
|
||||||
|
>(worker: Worker): Worker {
|
||||||
|
return Object.freeze({
|
||||||
|
...worker,
|
||||||
|
...('runtimes' in worker
|
||||||
|
? {
|
||||||
|
runtimes: Object.freeze(
|
||||||
|
worker.runtimes.map((runtime) => Object.freeze({ ...runtime })),
|
||||||
|
),
|
||||||
|
declaredCapacity: Object.freeze({ ...worker.declaredCapacity }),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
}) as Worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function projectWorkerSessionInspection(
|
||||||
|
projectId: string,
|
||||||
|
response: Readonly<ClusterWorkerManagementClientResult>,
|
||||||
|
): Readonly<WorkerSessionInspection> {
|
||||||
|
if (response.result.operation !== 'worker-session.inspect') {
|
||||||
|
throw new ClusterWorkerManagementProductError();
|
||||||
|
}
|
||||||
|
return Object.freeze({
|
||||||
|
schema: 'qinglong/worker-session-inspection@v1',
|
||||||
|
projectId: identifier(projectId),
|
||||||
|
observedAtMs: response.result.observedAtMs,
|
||||||
|
found: response.result.worker !== null,
|
||||||
|
worker:
|
||||||
|
response.result.worker === null
|
||||||
|
? null
|
||||||
|
: cloneWorker(response.result.worker),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function projectWorkerSessionList(
|
||||||
|
projectId: string,
|
||||||
|
response: Readonly<ClusterWorkerManagementClientResult>,
|
||||||
|
): Readonly<WorkerSessionList> {
|
||||||
|
if (response.result.operation !== 'worker-session.list') {
|
||||||
|
throw new ClusterWorkerManagementProductError();
|
||||||
|
}
|
||||||
|
const workers = Object.freeze(response.result.workers.map(cloneWorker));
|
||||||
|
return Object.freeze({
|
||||||
|
schema: 'qinglong/worker-session-list@v1',
|
||||||
|
projectId: identifier(projectId),
|
||||||
|
observedAtMs: response.result.observedAtMs,
|
||||||
|
count: workers.length,
|
||||||
|
workers,
|
||||||
|
nextAfterWorkerId: response.result.nextCursor,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWorkerSessionInspectionCard(
|
||||||
|
inspection: Readonly<WorkerSessionInspection>,
|
||||||
|
): string {
|
||||||
|
if (inspection.worker === null) {
|
||||||
|
return [
|
||||||
|
`Worker session: not found`,
|
||||||
|
`Project: ${inspection.projectId}`,
|
||||||
|
`Observed: ${inspection.observedAtMs}`,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
const worker = inspection.worker;
|
||||||
|
return [
|
||||||
|
`Worker session: ${worker.workerId}`,
|
||||||
|
`Project: ${inspection.projectId}`,
|
||||||
|
`State: ${worker.lifecycle} / ${worker.compatibility} / ${worker.supportTier}`,
|
||||||
|
`Platform: ${worker.operatingSystem ?? 'unknown'} ${
|
||||||
|
worker.architecture
|
||||||
|
} / protocol ${worker.protocolVersion}`,
|
||||||
|
`Capacity: ${worker.availableSlots}/${worker.maxConcurrentRuns} slots available`,
|
||||||
|
`Heartbeat: ${worker.lastHeartbeatAtMs} / lease ${worker.leaseExpiresAtMs}`,
|
||||||
|
`Observed: ${inspection.observedAtMs}`,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatWorkerSessionListCard(
|
||||||
|
page: Readonly<WorkerSessionList>,
|
||||||
|
): string {
|
||||||
|
const lines = [
|
||||||
|
`Worker sessions: ${page.count}`,
|
||||||
|
`Project: ${page.projectId}`,
|
||||||
|
`Observed: ${page.observedAtMs}`,
|
||||||
|
];
|
||||||
|
for (const worker of page.workers) {
|
||||||
|
lines.push(
|
||||||
|
`${worker.workerId} ${worker.lifecycle} ${worker.supportTier} ${worker.architecture} slots ${worker.availableSlots}/${worker.maxConcurrentRuns}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push(`Next after: ${page.nextAfterWorkerId ?? '-'}`);
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
@@ -284,6 +284,13 @@ function validContextFixture(t) {
|
|||||||
copilot: { configFile: copilotConfig },
|
copilot: { configFile: copilotConfig },
|
||||||
package: { configFile: packageConfig },
|
package: { configFile: packageConfig },
|
||||||
'package-kubernetes': { configFile: packageConfig, kubernetesFile },
|
'package-kubernetes': { configFile: packageConfig, kubernetesFile },
|
||||||
|
worker: {
|
||||||
|
configFile: config(
|
||||||
|
'worker-observation-client',
|
||||||
|
'/api/v3/workers/management',
|
||||||
|
'required',
|
||||||
|
),
|
||||||
|
},
|
||||||
'worker-credential': {
|
'worker-credential': {
|
||||||
configFile: config(
|
configFile: config(
|
||||||
'worker-client',
|
'worker-client',
|
||||||
@@ -329,7 +336,7 @@ function validContextFixture(t) {
|
|||||||
|
|
||||||
test('catalog exposes only reviewed product entrypoints from the same package', () => {
|
test('catalog exposes only reviewed product entrypoints from the same package', () => {
|
||||||
assert.equal(manifest.bin['ql3-cluster-admin'], 'dist/product-cli/cli.js');
|
assert.equal(manifest.bin['ql3-cluster-admin'], 'dist/product-cli/cli.js');
|
||||||
assert.equal(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, 11);
|
assert.equal(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, 12);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
new Set(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.map(({ name }) => name)).size,
|
new Set(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.map(({ name }) => name)).size,
|
||||||
QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length,
|
QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length,
|
||||||
@@ -617,7 +624,7 @@ test('validates the complete operator context offline without operational author
|
|||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
component: 'qinglong3-cluster-product-cli',
|
component: 'qinglong3-cluster-product-cli',
|
||||||
event: 'context_valid',
|
event: 'context_valid',
|
||||||
commandCount: 8,
|
commandCount: 9,
|
||||||
commands: [
|
commands: [
|
||||||
{ name: 'copilot', transport: 'https', clientCertificate: 'forbidden' },
|
{ name: 'copilot', transport: 'https', clientCertificate: 'forbidden' },
|
||||||
{ name: 'package', transport: 'https', clientCertificate: 'forbidden' },
|
{ name: 'package', transport: 'https', clientCertificate: 'forbidden' },
|
||||||
@@ -627,6 +634,11 @@ test('validates the complete operator context offline without operational author
|
|||||||
clientCertificate: 'forbidden',
|
clientCertificate: 'forbidden',
|
||||||
kubernetesAuthentication: 'token',
|
kubernetesAuthentication: 'token',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'worker',
|
||||||
|
transport: 'https',
|
||||||
|
clientCertificate: 'required',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'worker-credential',
|
name: 'worker-credential',
|
||||||
transport: 'https',
|
transport: 'https',
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const NEXT_CLIENT_KEY = resolve(
|
|||||||
'fixtures/management-service-key.pem',
|
'fixtures/management-service-key.pem',
|
||||||
);
|
);
|
||||||
const WORKER_PATH = '/api/v3/worker-credentials/management';
|
const WORKER_PATH = '/api/v3/worker-credentials/management';
|
||||||
|
const CANONICAL_WORKER_PATH = '/api/v3/workers/management';
|
||||||
|
|
||||||
function command() {
|
function command() {
|
||||||
return {
|
return {
|
||||||
@@ -223,7 +224,7 @@ async function requestWithClientIdentity(application, certificate, key) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
test('serves only the fixed Worker credential management route', async () => {
|
test('serves the canonical Worker route and exact credential compatibility alias', async () => {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
const application = await start(async (value, authentication) => {
|
const application = await start(async (value, authentication) => {
|
||||||
calls.push({ value, principal: await authentication.authenticate() });
|
calls.push({ value, principal: await authentication.authenticate() });
|
||||||
@@ -245,12 +246,15 @@ test('serves only the fixed Worker credential management route', async () => {
|
|||||||
type: 'user',
|
type: 'user',
|
||||||
id: 'cluster-reviewer',
|
id: 'cluster-reviewer',
|
||||||
});
|
});
|
||||||
|
const canonical = await request(application, CANONICAL_WORKER_PATH);
|
||||||
|
assert.equal(canonical.statusCode, 200);
|
||||||
|
assert.equal(canonical.body.result.operation, 'worker-credential.inspect');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
(await request(application, '/api/v3/plugin-packages/management'))
|
(await request(application, '/api/v3/plugin-packages/management'))
|
||||||
.statusCode,
|
.statusCode,
|
||||||
404,
|
404,
|
||||||
);
|
);
|
||||||
assert.equal(calls.length, 1);
|
assert.equal(calls.length, 2);
|
||||||
} finally {
|
} finally {
|
||||||
await application.close();
|
await application.close();
|
||||||
}
|
}
|
||||||
@@ -312,10 +316,18 @@ test('rejects a CRL-revoked client certificate before OIDC', async () => {
|
|||||||
|
|
||||||
test('maps Worker credential management failures to stable HTTP errors', async () => {
|
test('maps Worker credential management failures to stable HTTP errors', async () => {
|
||||||
for (const [failure, statusCode, code] of [
|
for (const [failure, statusCode, code] of [
|
||||||
[new WorkerCredentialManagementRequestError('invalid'), 400, 'request_invalid'],
|
[
|
||||||
|
new WorkerCredentialManagementRequestError('invalid'),
|
||||||
|
400,
|
||||||
|
'request_invalid',
|
||||||
|
],
|
||||||
[new WorkerCredentialManagementAuthorizationError(), 403, 'forbidden'],
|
[new WorkerCredentialManagementAuthorizationError(), 403, 'forbidden'],
|
||||||
[new WorkerCredentialManagementConflictError('conflict'), 409, 'conflict'],
|
[new WorkerCredentialManagementConflictError('conflict'), 409, 'conflict'],
|
||||||
[new WorkerCredentialManagementQuotaExceededError(1_250), 429, 'quota_exceeded'],
|
[
|
||||||
|
new WorkerCredentialManagementQuotaExceededError(1_250),
|
||||||
|
429,
|
||||||
|
'quota_exceeded',
|
||||||
|
],
|
||||||
[new WorkerCredentialManagementUnavailableError(), 503, 'unavailable'],
|
[new WorkerCredentialManagementUnavailableError(), 503, 'unavailable'],
|
||||||
]) {
|
]) {
|
||||||
const application = await start(async () => {
|
const application = await start(async () => {
|
||||||
@@ -353,6 +365,34 @@ test('rejects arbitrary management paths at configuration time', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rejects arbitrary or cross-plane compatible route aliases', async () => {
|
||||||
|
for (const compatibleManagementPaths of [
|
||||||
|
['/api/v3/automations/management'],
|
||||||
|
['/api/v3/worker-credentials/management', '/api/v3/runs/management'],
|
||||||
|
]) {
|
||||||
|
const privateKey = Buffer.from(readFileSync(SERVER_KEY));
|
||||||
|
try {
|
||||||
|
await assert.rejects(
|
||||||
|
startClusterPluginPackageManagementHttp({
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 0,
|
||||||
|
tls: {
|
||||||
|
privateKey,
|
||||||
|
certificate: Buffer.from(readFileSync(SERVER_CERT)),
|
||||||
|
},
|
||||||
|
identities: identities(),
|
||||||
|
transport: { async execute() {} },
|
||||||
|
managementPath: CANONICAL_WORKER_PATH,
|
||||||
|
compatibleManagementPaths,
|
||||||
|
}),
|
||||||
|
ClusterPluginPackageManagementHttpConfigurationError,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
privateKey.fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('accepts both client CAs during overlap then rejects the retired CA', async () => {
|
test('accepts both client CAs during overlap then rejects the retired CA', async () => {
|
||||||
const execute = async () => ({
|
const execute = async () => ({
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const {
|
||||||
|
executeClusterWorkerManagementClient,
|
||||||
|
normalizeClusterWorkerManagementCommand,
|
||||||
|
} = require('@qinglong/cluster-admin/worker-management-client');
|
||||||
|
const {
|
||||||
|
createWorkerSessionInspectionCommand,
|
||||||
|
createWorkerSessionListCommand,
|
||||||
|
formatWorkerSessionInspectionCard,
|
||||||
|
formatWorkerSessionListCard,
|
||||||
|
projectWorkerSessionInspection,
|
||||||
|
projectWorkerSessionList,
|
||||||
|
} = require('@qinglong/cluster-admin/worker-management-product');
|
||||||
|
|
||||||
|
const fixtureRoot = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'../../ql3-cluster-control/test/fixtures/mtls',
|
||||||
|
);
|
||||||
|
const detailedWorker = Object.freeze({
|
||||||
|
workerId: 'worker-a',
|
||||||
|
sessionId: 'session-a',
|
||||||
|
generation: 2,
|
||||||
|
sessionVersion: 5,
|
||||||
|
lifecycle: 'online',
|
||||||
|
compatibility: 'default_placement',
|
||||||
|
architecture: 'arm64',
|
||||||
|
supportTier: 'tier1',
|
||||||
|
protocolVersion: '1.0.0',
|
||||||
|
operatingSystem: 'linux',
|
||||||
|
maxConcurrentRuns: 2,
|
||||||
|
availableSlots: 1,
|
||||||
|
registeredAtMs: 900,
|
||||||
|
lastHeartbeatAtMs: 1_050,
|
||||||
|
leaseExpiresAtMs: 2_000,
|
||||||
|
updatedAtMs: 1_050,
|
||||||
|
observedAtMs: 1_100,
|
||||||
|
runtimes: Object.freeze([{ name: 'node', version: '24.18.0' }]),
|
||||||
|
declaredCapacity: Object.freeze({
|
||||||
|
cpuCores: 1,
|
||||||
|
memoryBytes: 268_435_456,
|
||||||
|
diskBytes: 1_073_741_824,
|
||||||
|
gpuCount: 0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const summaryWorker = Object.freeze(
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(detailedWorker).filter(
|
||||||
|
([key]) => key !== 'runtimes' && key !== 'declaredCapacity',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
function response(result) {
|
||||||
|
return Object.freeze({
|
||||||
|
schemaVersion: 1,
|
||||||
|
requestId: 'transport-request-must-not-project',
|
||||||
|
result: Object.freeze(result),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function privateFile(directory, name, value) {
|
||||||
|
const filePath = path.join(directory, name);
|
||||||
|
fs.writeFileSync(filePath, value, { mode: 0o600 });
|
||||||
|
return fs.realpathSync(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('builds only immutable inspect and bounded list commands', () => {
|
||||||
|
const inspect = createWorkerSessionInspectionCommand(
|
||||||
|
'project-a',
|
||||||
|
'worker-a',
|
||||||
|
() => 'inspection-a',
|
||||||
|
);
|
||||||
|
assert.deepEqual(inspect, {
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'worker-session.inspect',
|
||||||
|
request: {
|
||||||
|
authorityProjectId: 'project-a',
|
||||||
|
workerId: 'worker-a',
|
||||||
|
inspectionId: 'inspection-a',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(Object.isFrozen(inspect), true);
|
||||||
|
assert.equal(Object.isFrozen(inspect.request), true);
|
||||||
|
|
||||||
|
const page = createWorkerSessionListCommand(
|
||||||
|
'project-a',
|
||||||
|
'worker-a',
|
||||||
|
() => 'inspection-b',
|
||||||
|
);
|
||||||
|
assert.deepEqual(page.request, {
|
||||||
|
authorityProjectId: 'project-a',
|
||||||
|
afterWorkerId: 'worker-a',
|
||||||
|
inspectionId: 'inspection-b',
|
||||||
|
});
|
||||||
|
assert.equal(Object.hasOwn(page.request, 'limit'), false);
|
||||||
|
assert.throws(() => createWorkerSessionListCommand('../escape'));
|
||||||
|
assert.throws(() =>
|
||||||
|
normalizeClusterWorkerManagementCommand({
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'worker-credential.inspect',
|
||||||
|
request: {
|
||||||
|
actionRef: 'action-a',
|
||||||
|
authorityProjectId: 'project-a',
|
||||||
|
approvalRequestId: 'approval-a',
|
||||||
|
inspectionId: 'inspection-a',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('projects strict low-sensitive products without transport request identity', () => {
|
||||||
|
const inspection = projectWorkerSessionInspection(
|
||||||
|
'project-a',
|
||||||
|
response({
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'worker-session.inspect',
|
||||||
|
observedAtMs: 1_100,
|
||||||
|
worker: detailedWorker,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert.equal(inspection.schema, 'qinglong/worker-session-inspection@v1');
|
||||||
|
assert.equal(inspection.found, true);
|
||||||
|
assert.equal(inspection.worker.workerId, 'worker-a');
|
||||||
|
assert.equal(Object.isFrozen(inspection.worker.runtimes), true);
|
||||||
|
assert.doesNotMatch(
|
||||||
|
JSON.stringify(inspection),
|
||||||
|
/transport-request|inspectionId/,
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
formatWorkerSessionInspectionCard(inspection),
|
||||||
|
/slots available/,
|
||||||
|
);
|
||||||
|
|
||||||
|
const page = projectWorkerSessionList(
|
||||||
|
'project-a',
|
||||||
|
response({
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'worker-session.list',
|
||||||
|
observedAtMs: 1_100,
|
||||||
|
workers: [summaryWorker],
|
||||||
|
nextCursor: 'worker-a',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
{
|
||||||
|
schema: page.schema,
|
||||||
|
count: page.count,
|
||||||
|
nextAfterWorkerId: page.nextAfterWorkerId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schema: 'qinglong/worker-session-list@v1',
|
||||||
|
count: 1,
|
||||||
|
nextAfterWorkerId: 'worker-a',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.match(formatWorkerSessionListCard(page), /worker-a online/);
|
||||||
|
assert.throws(() =>
|
||||||
|
projectWorkerSessionInspection(
|
||||||
|
'project-a',
|
||||||
|
response({
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'worker-session.list',
|
||||||
|
observedAtMs: 1_100,
|
||||||
|
workers: [],
|
||||||
|
nextCursor: null,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requires the canonical Worker endpoint before making a connection', async (t) => {
|
||||||
|
const directory = fs.realpathSync(
|
||||||
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-worker-product-client-')),
|
||||||
|
);
|
||||||
|
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||||
|
const caFile = privateFile(
|
||||||
|
directory,
|
||||||
|
'ca.pem',
|
||||||
|
fs.readFileSync(path.join(fixtureRoot, 'ca-cert.pem')),
|
||||||
|
);
|
||||||
|
const clientCertificateFile = privateFile(
|
||||||
|
directory,
|
||||||
|
'client.crt',
|
||||||
|
fs.readFileSync(path.join(fixtureRoot, 'client-cert.pem')),
|
||||||
|
);
|
||||||
|
const clientPrivateKeyFile = privateFile(
|
||||||
|
directory,
|
||||||
|
'client.key',
|
||||||
|
fs.readFileSync(path.join(fixtureRoot, 'client-key.pem')),
|
||||||
|
);
|
||||||
|
const assertionFile = privateFile(
|
||||||
|
directory,
|
||||||
|
'assertion.jwt',
|
||||||
|
'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJ1In0.c2lnbmF0dXJl',
|
||||||
|
);
|
||||||
|
const command = createWorkerSessionListCommand(
|
||||||
|
'project-a',
|
||||||
|
undefined,
|
||||||
|
() => 'inspection-a',
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [managementPath, expectedConnections] of [
|
||||||
|
['/api/v3/workers/management', 1],
|
||||||
|
['/api/v3/worker-credentials/management', 0],
|
||||||
|
]) {
|
||||||
|
const configFile = privateFile(
|
||||||
|
directory,
|
||||||
|
`client-${expectedConnections}.json`,
|
||||||
|
JSON.stringify({
|
||||||
|
schemaVersion: 1,
|
||||||
|
endpoint: `https://manager.example.test:8443${managementPath}`,
|
||||||
|
servername: 'manager.example.test',
|
||||||
|
caFile,
|
||||||
|
clientCertificateFile,
|
||||||
|
clientPrivateKeyFile,
|
||||||
|
requestTimeoutMs: 1_000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let connections = 0;
|
||||||
|
await assert.rejects(
|
||||||
|
executeClusterWorkerManagementClient(
|
||||||
|
{ configFile, assertionFile, command },
|
||||||
|
{
|
||||||
|
async connect() {
|
||||||
|
connections += 1;
|
||||||
|
throw new Error('stop-after-policy-validation');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.equal(connections, expectedConnections);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { spawn } = require('node:child_process');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const { createServer } = require('node:https');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const packageRoot = path.resolve(__dirname, '..');
|
||||||
|
const cliPath = path.join(
|
||||||
|
packageRoot,
|
||||||
|
'dist',
|
||||||
|
'worker-management',
|
||||||
|
'workerManagementClientCli.js',
|
||||||
|
);
|
||||||
|
const fixtureRoot = path.resolve(
|
||||||
|
packageRoot,
|
||||||
|
'../ql3-cluster-control/test/fixtures/mtls',
|
||||||
|
);
|
||||||
|
|
||||||
|
function privateFile(directory, name, value) {
|
||||||
|
const filePath = path.join(directory, name);
|
||||||
|
fs.writeFileSync(filePath, value, { mode: 0o600 });
|
||||||
|
return fs.realpathSync(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCli(args) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(process.execPath, [cliPath, ...args], {
|
||||||
|
cwd: packageRoot,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
const stdout = [];
|
||||||
|
const stderr = [];
|
||||||
|
child.stdout.on('data', (chunk) => stdout.push(chunk));
|
||||||
|
child.stderr.on('data', (chunk) => stderr.push(chunk));
|
||||||
|
child.once('error', reject);
|
||||||
|
child.once('close', (status, signal) => {
|
||||||
|
resolve({
|
||||||
|
status,
|
||||||
|
signal,
|
||||||
|
stdout: Buffer.concat(stdout).toString('utf8'),
|
||||||
|
stderr: Buffer.concat(stderr).toString('utf8'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function summary(workerId) {
|
||||||
|
return {
|
||||||
|
workerId,
|
||||||
|
sessionId: `session-${workerId}`,
|
||||||
|
generation: 1,
|
||||||
|
sessionVersion: 1,
|
||||||
|
lifecycle: 'online',
|
||||||
|
compatibility: 'default_placement',
|
||||||
|
architecture: 'arm64',
|
||||||
|
supportTier: 'tier1',
|
||||||
|
protocolVersion: '1.0.0',
|
||||||
|
operatingSystem: 'linux',
|
||||||
|
maxConcurrentRuns: 1,
|
||||||
|
availableSlots: 1,
|
||||||
|
registeredAtMs: 900,
|
||||||
|
lastHeartbeatAtMs: 1_000,
|
||||||
|
leaseExpiresAtMs: 2_000,
|
||||||
|
updatedAtMs: 1_000,
|
||||||
|
observedAtMs: 1_100,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('CLI performs one canonical read per invocation and rejects mutation vocabulary', async (t) => {
|
||||||
|
const directory = fs.realpathSync(
|
||||||
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-worker-cli-')),
|
||||||
|
);
|
||||||
|
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||||
|
const requests = [];
|
||||||
|
const server = createServer(
|
||||||
|
{
|
||||||
|
key: fs.readFileSync(path.join(fixtureRoot, 'server-key.pem')),
|
||||||
|
cert: fs.readFileSync(path.join(fixtureRoot, 'server-cert.pem')),
|
||||||
|
ca: fs.readFileSync(path.join(fixtureRoot, 'ca-cert.pem')),
|
||||||
|
requestCert: true,
|
||||||
|
rejectUnauthorized: true,
|
||||||
|
minVersion: 'TLSv1.3',
|
||||||
|
maxVersion: 'TLSv1.3',
|
||||||
|
},
|
||||||
|
(request, response) => {
|
||||||
|
const chunks = [];
|
||||||
|
request.on('data', (chunk) => chunks.push(chunk));
|
||||||
|
request.on('end', () => {
|
||||||
|
const command = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
||||||
|
requests.push({
|
||||||
|
method: request.method,
|
||||||
|
path: request.url,
|
||||||
|
authorized: request.socket.authorized,
|
||||||
|
command,
|
||||||
|
});
|
||||||
|
const worker = summary('worker-a');
|
||||||
|
const result =
|
||||||
|
command.operation === 'worker-session.inspect'
|
||||||
|
? {
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'worker-session.inspect',
|
||||||
|
observedAtMs: 1_100,
|
||||||
|
worker: {
|
||||||
|
...worker,
|
||||||
|
runtimes: [{ name: 'node', version: '24.18.0' }],
|
||||||
|
declaredCapacity: {
|
||||||
|
cpuCores: 1,
|
||||||
|
memoryBytes: 268_435_456,
|
||||||
|
diskBytes: 1_073_741_824,
|
||||||
|
gpuCount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
schemaVersion: 1,
|
||||||
|
operation: 'worker-session.list',
|
||||||
|
observedAtMs: 1_100,
|
||||||
|
workers: [worker],
|
||||||
|
nextCursor: null,
|
||||||
|
};
|
||||||
|
const body = Buffer.from(
|
||||||
|
JSON.stringify({
|
||||||
|
schemaVersion: 1,
|
||||||
|
requestId: 'server-request-hidden',
|
||||||
|
result,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
response.writeHead(200, {
|
||||||
|
'content-type': 'application/json; charset=utf-8',
|
||||||
|
'content-length': String(body.length),
|
||||||
|
});
|
||||||
|
response.end(body);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
server.once('error', reject);
|
||||||
|
server.listen(0, '127.0.0.1', resolve);
|
||||||
|
});
|
||||||
|
t.after(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
server.close(() => resolve());
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const address = server.address();
|
||||||
|
assert.notEqual(address, null);
|
||||||
|
assert.notEqual(typeof address, 'string');
|
||||||
|
const caFile = privateFile(
|
||||||
|
directory,
|
||||||
|
'ca.pem',
|
||||||
|
fs.readFileSync(path.join(fixtureRoot, 'ca-cert.pem')),
|
||||||
|
);
|
||||||
|
const clientCertificateFile = privateFile(
|
||||||
|
directory,
|
||||||
|
'client.crt',
|
||||||
|
fs.readFileSync(path.join(fixtureRoot, 'client-cert.pem')),
|
||||||
|
);
|
||||||
|
const clientPrivateKeyFile = privateFile(
|
||||||
|
directory,
|
||||||
|
'client.key',
|
||||||
|
fs.readFileSync(path.join(fixtureRoot, 'client-key.pem')),
|
||||||
|
);
|
||||||
|
const configFile = privateFile(
|
||||||
|
directory,
|
||||||
|
'client.json',
|
||||||
|
JSON.stringify({
|
||||||
|
schemaVersion: 1,
|
||||||
|
endpoint: `https://localhost:${address.port}/api/v3/workers/management`,
|
||||||
|
servername: 'localhost',
|
||||||
|
caFile,
|
||||||
|
clientCertificateFile,
|
||||||
|
clientPrivateKeyFile,
|
||||||
|
requestTimeoutMs: 2_000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const assertionFile = privateFile(
|
||||||
|
directory,
|
||||||
|
'assertion.jwt',
|
||||||
|
'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJ1In0.c2lnbmF0dXJl',
|
||||||
|
);
|
||||||
|
|
||||||
|
const inspect = await runCli([
|
||||||
|
'inspect',
|
||||||
|
`--config=${configFile}`,
|
||||||
|
`--assertion=${assertionFile}`,
|
||||||
|
'--project=project-a',
|
||||||
|
'--worker=worker-a',
|
||||||
|
'--format=json',
|
||||||
|
]);
|
||||||
|
assert.equal(inspect.status, 0, inspect.stderr);
|
||||||
|
assert.equal(inspect.stderr, '');
|
||||||
|
const inspection = JSON.parse(inspect.stdout);
|
||||||
|
assert.equal(inspection.schema, 'qinglong/worker-session-inspection@v1');
|
||||||
|
assert.equal(inspection.worker.workerId, 'worker-a');
|
||||||
|
assert.equal(inspect.stdout.includes('server-request-hidden'), false);
|
||||||
|
|
||||||
|
const list = await runCli([
|
||||||
|
'list',
|
||||||
|
`--config=${configFile}`,
|
||||||
|
`--assertion=${assertionFile}`,
|
||||||
|
'--project=project-a',
|
||||||
|
'--format=json',
|
||||||
|
]);
|
||||||
|
assert.equal(list.status, 0, list.stderr);
|
||||||
|
assert.equal(JSON.parse(list.stdout).count, 1);
|
||||||
|
assert.equal(requests.length, 2);
|
||||||
|
assert.deepEqual(
|
||||||
|
requests.map(({ method, path, authorized, command }) => ({
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
authorized,
|
||||||
|
operation: command.operation,
|
||||||
|
})),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
path: '/api/v3/workers/management',
|
||||||
|
authorized: true,
|
||||||
|
operation: 'worker-session.inspect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
path: '/api/v3/workers/management',
|
||||||
|
authorized: true,
|
||||||
|
operation: 'worker-session.list',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const rejected = await runCli([
|
||||||
|
'inspect',
|
||||||
|
`--config=${configFile}`,
|
||||||
|
`--assertion=${assertionFile}`,
|
||||||
|
'--project=project-a',
|
||||||
|
'--worker=worker-a',
|
||||||
|
'--command=/private/mutation.json',
|
||||||
|
]);
|
||||||
|
assert.equal(rejected.status, 64);
|
||||||
|
assert.equal(requests.length, 2);
|
||||||
|
assert.equal(rejected.stdout, '');
|
||||||
|
assert.equal(JSON.parse(rejected.stderr).event, 'usage_invalid');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CLI help states its bounded read contract', async () => {
|
||||||
|
const help = await runCli(['--help']);
|
||||||
|
assert.equal(help.status, 0);
|
||||||
|
assert.match(help.stdout, /ql3-worker-client inspect/);
|
||||||
|
assert.match(help.stdout, /ql3-worker-client list/);
|
||||||
|
assert.match(help.stdout, /never retries, polls or auto-pages/);
|
||||||
|
assert.doesNotMatch(help.stdout, /credential|secret|token/i);
|
||||||
|
});
|
||||||
@@ -475,6 +475,8 @@ function assertExactExternalClosure(readFile, root, findings) {
|
|||||||
'dist/worker-credential/management-server/workerCredentialManagementCli.js' ||
|
'dist/worker-credential/management-server/workerCredentialManagementCli.js' ||
|
||||||
adminManifest.bin?.['ql3-worker-credential-client'] !==
|
adminManifest.bin?.['ql3-worker-credential-client'] !==
|
||||||
'dist/worker-credential/workerCredentialManagementClientCli.js' ||
|
'dist/worker-credential/workerCredentialManagementClientCli.js' ||
|
||||||
|
adminManifest.bin?.['ql3-worker-client'] !==
|
||||||
|
'dist/worker-management/workerManagementClientCli.js' ||
|
||||||
adminManifest.bin?.['ql3-worker-credential-execute'] !==
|
adminManifest.bin?.['ql3-worker-credential-execute'] !==
|
||||||
'dist/worker-credential/workerCredentialExecutorCli.js' ||
|
'dist/worker-credential/workerCredentialExecutorCli.js' ||
|
||||||
adminManifest.exports?.['./plugin-package-recovery-process']?.require !==
|
adminManifest.exports?.['./plugin-package-recovery-process']?.require !==
|
||||||
@@ -498,13 +500,17 @@ function assertExactExternalClosure(readFile, root, findings) {
|
|||||||
adminManifest.exports?.['./worker-credential-management-client']
|
adminManifest.exports?.['./worker-credential-management-client']
|
||||||
?.require !==
|
?.require !==
|
||||||
'./dist/worker-credential/workerCredentialManagementClient.js' ||
|
'./dist/worker-credential/workerCredentialManagementClient.js' ||
|
||||||
|
adminManifest.exports?.['./worker-management-client']?.require !==
|
||||||
|
'./dist/worker-management/workerManagementClient.js' ||
|
||||||
|
adminManifest.exports?.['./worker-management-product']?.require !==
|
||||||
|
'./dist/worker-management/workerManagementProduct.js' ||
|
||||||
adminManifest.exports?.['./worker-credential-executor-process']?.require !==
|
adminManifest.exports?.['./worker-credential-executor-process']?.require !==
|
||||||
'./dist/worker-credential/workerCredentialExecutorProcess.js'
|
'./dist/worker-credential/workerCredentialExecutorProcess.js'
|
||||||
) {
|
) {
|
||||||
findings.push(
|
findings.push(
|
||||||
finding(
|
finding(
|
||||||
'QL3_CLUSTER_PLUGIN_RECOVERY_ENTRYPOINT_MISSING',
|
'QL3_CLUSTER_PLUGIN_RECOVERY_ENTRYPOINT_MISSING',
|
||||||
'cluster-admin must publish the reviewed product facade, Package and Worker management and executor entrypoints',
|
'cluster-admin must publish the reviewed product facade, Package and credential-compatible/read-only Worker management and executor entrypoints',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ test('ships a path-only Cluster operator context example without durable authori
|
|||||||
'package',
|
'package',
|
||||||
'package-kubernetes',
|
'package-kubernetes',
|
||||||
'run',
|
'run',
|
||||||
|
'worker',
|
||||||
'worker-credential',
|
'worker-credential',
|
||||||
]);
|
]);
|
||||||
for (const [name, command] of Object.entries(example.commands)) {
|
for (const [name, command] of Object.entries(example.commands)) {
|
||||||
@@ -753,13 +754,16 @@ test('rejects widened authority or lifecycle in the Worker management client', (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('requires the production Worker credential management client export and binary', () => {
|
test('requires both read-only Worker and credential-compatible client entrypoints', () => {
|
||||||
const report = auditClusterDeployment({
|
const report = auditClusterDeployment({
|
||||||
root: ROOT,
|
root: ROOT,
|
||||||
readFile: intercept('packages/ql3-cluster-admin/package.json', (source) => {
|
readFile: intercept('packages/ql3-cluster-admin/package.json', (source) => {
|
||||||
const manifest = JSON.parse(source);
|
const manifest = JSON.parse(source);
|
||||||
delete manifest.bin['ql3-worker-credential-client'];
|
delete manifest.bin['ql3-worker-credential-client'];
|
||||||
|
delete manifest.bin['ql3-worker-client'];
|
||||||
delete manifest.exports['./worker-credential-management-client'];
|
delete manifest.exports['./worker-credential-management-client'];
|
||||||
|
delete manifest.exports['./worker-management-client'];
|
||||||
|
delete manifest.exports['./worker-management-product'];
|
||||||
return JSON.stringify(manifest);
|
return JSON.stringify(manifest);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -340,10 +340,10 @@ test('current QL3 workspace has exactly eighteen reviewed package boundaries', (
|
|||||||
rootSourceFileRoles: clusterAdmin.rootSourceFileRoles,
|
rootSourceFileRoles: clusterAdmin.rootSourceFileRoles,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sourceFiles: 125,
|
sourceFiles: 128,
|
||||||
rootSourceFiles: 1,
|
rootSourceFiles: 1,
|
||||||
rootSourceLines: 61,
|
rootSourceLines: 61,
|
||||||
nestedSourceFiles: 124,
|
nestedSourceFiles: 127,
|
||||||
rootSourceFileRoles: {
|
rootSourceFileRoles: {
|
||||||
'modelInvocationMigrationCli.ts': 'binary_entry',
|
'modelInvocationMigrationCli.ts': 'binary_entry',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user