fix: enforce k3s ssh route syntax
This commit is contained in:
@@ -19,11 +19,11 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI
|
||||
- `server cleanup plan [--min-age-hours N] [--limit N]` 只生成主 server Docker 镜像清理 dry-run 计划,不执行删除;默认 `--min-age-hours 24`,避免把刚发布或刚验证的镜像列为 stale。输出必须包含 `dryRun=true`、`mutation=false`、`policy.deletionExecuted=false`、active containers/images、受保护镜像、candidate stale images、估算释放空间、风险等级、`commandsToReview` 和人工审批清单。计划必须保守白名单:保留 running containers 使用的 image ID,保留 stopped containers 引用的 image ID 直到人工先复核容器,保留 `deploy.json`/`CI.json` 当前 commit-pinned artifact、Compose stable image、上游 digest pin 和 provider-gateway runner image;`protectedStorage` 必须显式列出 PostgreSQL named volume、Baidu Netdisk `.state`、D601 registry storage 和 Docker volumes/host data policy。该入口禁止生成或执行 `docker system prune`、`docker image prune`、`docker builder prune`、`docker volume rm`、`docker compose down -v`、数据库清理或 host data `rm` 命令;未来若增加真实删除,必须另设显式审批参数并先复核 dry-run 输出。
|
||||
- `server rebuild <backend-core|frontend|dev-frontend-proxy|provider-gateway|todo-note|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>` 创建异步 job,先构建目标服务镜像,随后在 `.state/locks/server-compose.lock` 串行保护下用 `--no-deps --force-recreate` 替换目标 service 并等待容器 `healthy/running`;该命令用于替代手工删除容器的兜底流程,其中 `dev-frontend-proxy` 只更新主 server dev 入口薄代理,`todo-note`、`code-queue-mgr`、`project-manager`、`baidu-netdisk` 和 `oa-event-flow` 只重建主 server 承载的对应后端,不会重建或删除 database 命名卷。D601 Code Queue 执行面不由 `server rebuild` 管理,Rust backend-core 迭代不得用 `server rebuild backend-core` 在 master server 编译,规则见 `docs/reference/dev-environment.md`。
|
||||
- `provider attach <providerId> [--master-server URL] [--up] [--force]` 在新计算节点生成两项配置的 provider-gateway 挂载包:`.state/provider-<ID>.env` 默认只包含 `UNIDESK_MASTER_SERVER` 与 `PROVIDER_ID`,`provider-<ID>.yml` 固定 Docker socket、`pid: "host"`、`restart: always`、只读 `/workspace` 和 SSH 维护私钥挂载;`--up` 会立即执行生成的 `docker compose up -d --build`。`provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]` 是只读多信号健康裁决入口,会把单路径 `provider is not online`、SSH 超时、registry 失败和 service proxy 失败归类成 `runner-local-observation-gap`、`service-degraded`、`provider-degraded` 或 `global-blocker`。默认输出只返回裁决、scope、失败/降级/未知信号和有界 evidence 摘要,完整 evidence 必须显式加 `--full` 或 `--raw`;推荐交叉验证命令仍包含 `debug health`、`debug dispatch <providerId> host.ssh --wait-ms 15000`、`ssh <providerId> argv true`、`artifact-registry health --provider-id <providerId>`、`microservice health k3sctl-adapter`、`microservice health code-queue` 和 `codex tasks --view supervisor --limit 20`。
|
||||
- `ssh <route> [operation args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点;`route` 基础形态是 provider id,例如 `D601`,也可以扩展为纯定位路径 `provider:plane[:namespace:resource[:container]]`,例如 `D601:k3s` 或 `D601:k3s:hwlab-dev:hwlab-cloud-api`。非交互远端命令优先使用 `ssh <providerId> argv ...`;需要 shell 脚本、管道、变量或循环时优先使用 quoted heredoc 单步传输,例如 `bun scripts/cli.ts ssh D601 script <<'SCRIPT'`、`bun scripts/cli.ts ssh D601:k3s script <<'SCRIPT'` 或 `bun scripts/cli.ts ssh D601:k3s:<namespace>:<workload> script <<'SCRIPT'`,把脚本走 stdin,而不是把脚本压成多层引号字符串。需要在 pod 内改文件时优先使用 `D601:k3s:<namespace>:<workload> apply-patch`,CLI 会临时注入 pod 内 `apply_patch` helper 并把 patch stdin 交给它。ssh-like 命令遇到 timeout/kex/255 类失败时,CLI 会在 stderr 追加一行 `UNIDESK_SSH_HINT` JSON,提示 stdin script/argv 重试和 provider triage 交叉验证。
|
||||
- `ssh <route> [operation args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点;`route` 基础形态是 provider id,例如 `D601` 或 `G14`,也可以扩展为纯定位路径 `provider:plane[:namespace:resource[:container]]`,例如 `G14:k3s`、`D601:k3s` 或 `G14:k3s:<namespace>:<workload>`。非交互远端命令优先使用 `ssh <providerId> argv ...`;需要 shell 脚本、管道、变量或循环时优先使用 quoted heredoc 单步传输,例如 `bun scripts/cli.ts ssh G14 script <<'SCRIPT'`、`bun scripts/cli.ts ssh G14:k3s script <<'SCRIPT'` 或 `bun scripts/cli.ts ssh G14:k3s:<namespace>:<workload> script <<'SCRIPT'`,把脚本走 stdin,而不是把脚本压成多层引号字符串。需要在 pod 内改文件时优先使用 `<provider>:k3s:<namespace>:<workload> apply-patch`,CLI 会临时注入 pod 内 `apply_patch` helper 并把 patch stdin 交给它。ssh-like 命令遇到 timeout/kex/255 类失败时,CLI 会在 stderr 追加一行 `UNIDESK_SSH_HINT` JSON,提示 stdin script/argv 重试和 provider triage 交叉验证。
|
||||
- `ssh <providerId> apply-patch [tool args...] < patch.diff` 直接调用远端注入的 `apply_patch` 工具,并把本地 stdin 中的标准 `*** Begin Patch` / `*** End Patch` patch 流透传给目标节点。
|
||||
- `ssh <providerId> py [script-args...] < script.py` 把本地 stdin 落到远端临时 `.py` 文件后再以 `python3 -u` 执行并自动清理,避免再手写 `'python3 -'`、heredoc 或多层引号;`script-args` 会按 argv 安全透传给远端脚本。
|
||||
- `ssh <providerId> skills [--scope all|wsl|windows] [--limit N]` 发现目标节点上的 WSL/Linux skill 根目录;当 provider 是 WSL 时同一次调用还会扫描 Windows 用户目录下的 `.agents/skills` 与 `.codex/skills`。
|
||||
- `ssh D601:k3s[:namespace:workload[:container]] <operation> ...` 是 D601 原生 k3s 结构化 route 入口,route 只定位控制面或 workload,`kubectl`、`logs`、`exec`、`script`、`apply-patch` 和普通容器命令作为 operation 放在 route 之后;CLI 固定注入 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml` 并把 kubectl、workload exec 和 logs 参数组装成 argv,避免在 Host SSH、bash、kubectl exec 和容器 shell 之间反复手写多层引号。
|
||||
- `ssh <providerId>:k3s[:namespace:workload[:container]] <operation> ...` 是原生 k3s 结构化 route 入口,route 只定位控制面或 workload,`kubectl`、`logs`、`exec`、`script`、`apply-patch` 和普通容器命令作为 operation 放在 route 之后;CLI 固定注入 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml` 并把 kubectl、workload exec 和 logs 参数组装成 argv,避免在 Host SSH、bash、kubectl exec 和容器 shell 之间反复手写多层引号;D601 与 G14 都有 provider-specific guard,分别校验 `d601` 和 G14 k3s 节点身份。
|
||||
- `microservice list/status/health/diagnostics/tunnel-self-test/proxy` 通过 backend-core 内网 API 管理挂载在计算节点 Docker 或 k3s 控制面中的用户服务(底层命令名仍为 microservice);`health`、`status` 和 `diagnostics` 默认返回 compact summary、body 字节数和 `--full|--raw` 展开命令,只有小 body 或无法抽取 summary 时才带有界 body preview,避免 Code Queue/k3s 诊断一次性输出爆炸;`tunnel-self-test` 和 `proxy` 会走真实 backend-core -> provider-gateway 或 k3sctl-adapter -> 节点服务链路。`microservice health code-queue` 使用 commander-safe 专用摘要,必须保留 ok/status、service id、running count、queue count、heartbeat freshness/risk、split-brain/live/degraded 解释和 raw drill-down 命令;需要完整健康 JSON 时显式加 `--raw` 或 `--full`,等价深挖路径是 `microservice proxy code-queue /health --raw --full`。`proxy` 支持受控 JSON 请求体并对超大响应 body 默认输出有界预览,规则见 `docs/reference/microservices.md`。
|
||||
- `decision upload/list/show/health` 通过 backend-core 用户服务代理访问 D601 k3s Decision Center,用于上传会议记录/决议 Markdown、列出权威记录、查看详情和健康检查;`decision list` 默认只返回摘要并省略完整 Markdown body,需要排查大正文时显式加 `--include-body`。正式文书字段通过 records 模型一等字段返回和查询:`--doc-no DC-...`、`--doc-type DCSN|GOAL|PLAN|RPRT|ACTN|ISSU|RETR|RQST|RESP|MINS`、`--doc-priority P0|P1|P2|P3`、`--year YYYY`、`--signer`、`--issued-at`、`--effective-scope`、`--supersedes`、`--superseded-by`;`show` 和 `requirement update` 可使用 `id` 或 `docNo`。`decision requirement list/create/upsert/update/show` 在同一 records 模型上管理 `goal|decision|blocker|debt|experiment` 需求记录,`docNo` 唯一,未传 `--doc-no` 但提供 `--doc-type/--doc-priority/--year` 时由服务分配下一个序号。它们不得直连 D601 Service、NodePort 或 provider-gateway 业务 HTTP。
|
||||
- `decision diary import <markdown-file>` 将带 `# YYYY年M月D日`、`# YYYY-MM-DD` 或 `# YYYY/M/D` 标题的工作日志拆成每天一篇 Markdown 日记,按 `YYYY-MM/YYYY-MM-DD.md` 虚拟路径写入 Decision Center PostgreSQL;`decision diary list/history` 默认只返回摘要,需要完整 Markdown 时显式加 `--include-body`;`decision diary show <YYYY-MM-DD|id> [--source-file path]` 查看单日正文,`--source-file` 用于同一天存在多个导入来源时精确选择;`decision diary edit|upsert <YYYY-MM-DD|id> --body-file <path> [--title text] [--source-file path] [--tag tag]` 通过 `PUT /api/diary/entries/:idOrDate` 创建当天或历史条目并编辑既有条目。
|
||||
@@ -177,13 +177,17 @@ bun scripts/cli.ts ssh D601 find /home/ubuntu --max-depth 4 --type d --icontains
|
||||
bun scripts/cli.ts ssh D601 glob --root /home/ubuntu/pikapython --pattern '**/*-test.cpp' --limit 20 --sort
|
||||
```
|
||||
|
||||
`ssh` 的 route 语法是 `provider:plane[:namespace:resource[:container]]`,只负责定位分布式目标,不表达操作。当前稳定 plane 是 `k3s`:`D601:k3s` 定位到 D601 原生 k3s 控制面;`D601:k3s:<namespace>:<workload>[:container]` 定位到 namespace 下的一个默认 deployment workload;若目标是具体 Pod,workload 段写成 `pod/<podid>`,若目标是 Deployment,也可以显式写 `deployment/<name>` 或简写 `<name>`。`kubectl`、`logs`、`script`、`apply-patch`、`exec` 和普通容器命令都是 route 后面的 operation,这样路由子模块和操作子模块可以独立扩展。
|
||||
`ssh` 的 route 语法是 `provider:plane[:namespace:resource[:container]]`,只负责定位分布式目标,不表达操作。当前稳定 plane 是 `k3s`:`D601:k3s` 或 `G14:k3s` 定位到对应 provider 的原生 k3s 控制面;`<provider>:k3s:<namespace>:<workload>[:container]` 定位到 namespace 下的一个默认 deployment workload;若目标是具体 Pod,workload 段写成 `pod/<podid>`,若目标是 Deployment,也可以显式写 `deployment/<name>` 或简写 `<name>`。`kubectl`、`logs`、`script`、`apply-patch`、`exec` 和普通容器命令都是 route 后面的 operation,这样路由子模块和操作子模块可以独立扩展。
|
||||
|
||||
该入口解决运行面调试中最常见的多层 shell 引号问题。它不要求升级 provider-gateway,也不新增业务 API,只复用现有 Host SSH 维护桥;CLI 在本地把 Kubernetes 目标、namespace、container、log 限制、容器命令、stdin script 和 pod apply-patch 组装成 kubectl argv,并固定远端 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`。`D601:k3s` 无后续参数时执行 native k3s guard;`D601:k3s kubectl ...` 接收原始 kubectl argv;`D601:k3s script` 执行带 native kubeconfig 的 D601 host stdin 脚本;`D601:k3s:<namespace>:<workload> logs` 读取有界日志;`D601:k3s:<namespace>:<workload> exec ...` 和 `D601:k3s:<namespace>:<workload> <command> ...` 进入目标 workload;`D601:k3s:<namespace>:<workload> script` 把本地 stdin 作为 pod 内 shell 脚本执行;`D601:k3s:<namespace>:<workload> apply-patch` 把本地标准 patch 作为 stdin 送入 pod 内 `apply_patch`。典型用法:
|
||||
`k3s` 必须出现在 route 的 plane 段里,禁止使用 `ssh G14 k3s ...` 或 `ssh D601 k3s ...` 这类 post-provider shorthand;正确形态是 `ssh G14:k3s kubectl ...` 或 `ssh D601:k3s kubectl ...`。定位和操作必须保持分离,新增分布式目标时也按 `provider:plane[:scope...]` 扩展 route,而不是在 operation args 中新增另一套定位语法。
|
||||
|
||||
该入口解决运行面调试中最常见的多层 shell 引号问题。它不要求升级 provider-gateway,也不新增业务 API,只复用现有 Host SSH 维护桥;CLI 在本地把 Kubernetes 目标、namespace、container、log 限制、容器命令、stdin script 和 pod apply-patch 组装成 kubectl argv,并固定远端 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`。`<provider>:k3s` 无后续参数时执行 native k3s guard;`<provider>:k3s kubectl ...` 接收原始 kubectl argv;`<provider>:k3s script` 执行带 native kubeconfig 的 host stdin 脚本;`<provider>:k3s:<namespace>:<workload> logs` 读取有界日志;`<provider>:k3s:<namespace>:<workload> exec ...` 和 `<provider>:k3s:<namespace>:<workload> <command> ...` 进入目标 workload;`<provider>:k3s:<namespace>:<workload> script` 把本地 stdin 作为 pod 内 shell 脚本执行;`<provider>:k3s:<namespace>:<workload> apply-patch` 把本地标准 patch 作为 stdin 送入 pod 内 `apply_patch`。典型用法:
|
||||
|
||||
```bash
|
||||
bun scripts/cli.ts ssh D601:k3s
|
||||
bun scripts/cli.ts ssh D601:k3s kubectl get pods -n hwlab-dev
|
||||
bun scripts/cli.ts ssh G14:k3s
|
||||
bun scripts/cli.ts ssh G14:k3s kubectl get pipelineruns -n hwlab-ci
|
||||
printf 'kubectl get deploy -n hwlab-dev\n' | bun scripts/cli.ts ssh D601:k3s script
|
||||
bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api logs --tail 80
|
||||
bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api node -e 'console.log(process.version)'
|
||||
@@ -202,7 +206,7 @@ PATCH
|
||||
|
||||
`ssh <providerId> argv <command> [args...]` 是通用 argv 安全拼接入口;`exec` 是同义入口。它是非交互远端单进程命令的默认成功路径,不需要 shell 管道时直接传命令和参数,例如 `bun scripts/cli.ts ssh D601 argv true`。需要管道、重定向、变量展开或多条命令时,优先改用 `ssh <providerId> script <<'SCRIPT'`。`find`、`glob` 和 `apply-patch` 有专用入口;`rg`、`grep`、`sed`、`nl`、`stat`、`du`、`ls`、`cat`、`head`、`tail`、`wc` 和 `pwd` 可以直接作为 `ssh` 子命令使用,CLI 会对每个 argv token 做 shell quoting。旧的自由 ssh-like 远端命令入口只保留为近似原生 ssh 的人工兼容路径。
|
||||
|
||||
通过 `ssh <providerId>` 执行多行脚本时,优先使用结构化 helper,例如 `bun scripts/cli.ts ssh D601 py < script.py`、`bun scripts/cli.ts ssh D601 script <<'SCRIPT'` 或 `bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api script <<'SCRIPT'`。不要在远端命令字符串里再嵌套 heredoc、复杂引号或 `ssh 'python3 - <<EOF ...'` 形态;多层 shell 解析容易把 stdin 绑定到错误进程,结果会打开远端交互解释器并留下悬挂的 broker/SSH 会话。长脚本需要复用时,优先提交到 repo 或通过 stdin 传输到目标节点执行。
|
||||
通过 `ssh <providerId>` 执行多行脚本时,优先使用结构化 helper,例如 `bun scripts/cli.ts ssh G14 py < script.py`、`bun scripts/cli.ts ssh G14 script <<'SCRIPT'` 或 `bun scripts/cli.ts ssh G14:k3s script <<'SCRIPT'`。不要在远端命令字符串里再嵌套 heredoc、复杂引号或 `ssh 'python3 - <<EOF ...'` 形态;多层 shell 解析容易把 stdin 绑定到错误进程,结果会打开远端交互解释器并留下悬挂的 broker/SSH 会话。长脚本需要复用时,优先提交到 repo 或通过 stdin 传输到目标节点执行。
|
||||
|
||||
## Remote Main Server Passthrough
|
||||
|
||||
|
||||
+6
-3
@@ -19,14 +19,14 @@ export function rootHelp(): unknown {
|
||||
{ command: "server cleanup plan [--min-age-hours N] [--limit N]", description: "Dry-run Docker image cleanup plan only: list active/protected images, stale candidates older than the default 24h threshold, risk, estimated reclaim, and manual review commands without deleting anything." },
|
||||
{ command: "server rebuild <backend-core|frontend|dev-frontend-proxy|provider-gateway|todo-note|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>", description: "Maintenance-only local Compose rebuild for reviewed main-server services; frontend standard release must use CI artifact plus deploy apply dev/prod artifact consumers." },
|
||||
{ command: "provider attach <providerId> [--master-server URL] [--up] [--force] | provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]", description: "Generate the minimal external provider-gateway env/compose bundle or run the low-noise read-only provider health triage contract." },
|
||||
{ command: "ssh <route> [operation args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge; route syntax such as `D601:k3s:hwlab-dev:hwlab-cloud-api` only locates distributed targets." },
|
||||
{ command: "ssh <route> [operation args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge; route syntax such as `G14:k3s` only locates distributed targets." },
|
||||
{ command: "ssh <providerId> apply-patch [tool args...] < patch.diff", description: "Invoke the injected remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." },
|
||||
{ command: "ssh <providerId> py [script-args...] < script.py", description: "Run remote Python from local stdin through SSH passthrough without nested shell quoting; extra args become script argv." },
|
||||
{ command: "ssh <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT' ...", description: "Run a remote shell script from local stdin using shell -s, avoiding shell-command strings and nested remote heredocs." },
|
||||
{ command: "ssh <providerId> skills [--scope all|wsl|windows] [--limit N]", description: "Discover WSL/Linux and, for WSL providers, Windows skill directories in one SSH passthrough call." },
|
||||
{ command: "ssh <providerId> find <path...> [--max-depth N] [--type d|f|l] [--contains TEXT] [--iname PATTERN] [--limit N] [--sort]", description: "Run a structured remote find command without nested shell quoting or parentheses." },
|
||||
{ command: "ssh <providerId> glob [--root DIR] [--pattern PATTERN] [--contains TEXT] [--type any|f|d] [--limit N] [--sort]", description: "Run remote glob matching through the injected helper without shell glob expansion." },
|
||||
{ command: "ssh D601:k3s[:namespace:workload[:container]] <kubectl|logs|exec|script|apply-patch|command> ...", description: "Locate the D601 native k3s control plane or workload with route syntax, then run a separate operation with KUBECONFIG fixed and argv assembled by the CLI." },
|
||||
{ command: "ssh <providerId>:k3s[:namespace:workload[:container]] <kubectl|logs|exec|script|apply-patch|command> ...", description: "Locate a native k3s control plane or workload with route syntax, then run a separate operation with KUBECONFIG fixed and argv assembled by the CLI." },
|
||||
{ command: "ssh <providerId> argv <command> [args...]", description: "Run a non-interactive remote command with each argv token shell-quoted by UniDesk before SSH passthrough; use `ssh <providerId> script` when shell features are required." },
|
||||
{ command: "microservice list", description: "List UniDesk-managed user services and their provider/runtime mapping." },
|
||||
{ command: "microservice status <id>", description: "Show one user service config, repository reference, backend mapping, and runtime status." },
|
||||
@@ -155,6 +155,8 @@ export function sshHelp(): unknown {
|
||||
"bun scripts/cli.ts ssh <providerId> glob [--root DIR] [--pattern PATTERN]",
|
||||
"bun scripts/cli.ts ssh D601:k3s",
|
||||
"bun scripts/cli.ts ssh D601:k3s kubectl get pods -n hwlab-dev",
|
||||
"bun scripts/cli.ts ssh G14:k3s",
|
||||
"bun scripts/cli.ts ssh G14:k3s kubectl get pipelineruns -n hwlab-ci",
|
||||
"bun scripts/cli.ts ssh D601:k3s script <<'SCRIPT'",
|
||||
"bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api apply-patch <<'PATCH'",
|
||||
"bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api node -e 'console.log(process.version)'",
|
||||
@@ -164,7 +166,8 @@ export function sshHelp(): unknown {
|
||||
notes: [
|
||||
"ssh --help and ssh <route> --help print this JSON help and never open an interactive session.",
|
||||
"For non-interactive remote commands, prefer argv for a single process and script/stdin for shell logic.",
|
||||
"Route syntax is provider:plane[:namespace:resource[:container]] and locates a distributed target only. For D601 native k3s, D601:k3s locates the control plane, D601:k3s:<namespace>:<workload> locates a workload, and kubectl/script/logs/apply-patch/exec are operations placed after the route.",
|
||||
"Route syntax is provider:plane[:namespace:resource[:container]] and locates a distributed target only. For native k3s providers such as D601 and G14, <provider>:k3s locates the control plane, <provider>:k3s:<namespace>:<workload> locates a workload, and kubectl/script/logs/apply-patch/exec are operations placed after the route.",
|
||||
"Do not use post-provider shorthand such as `ssh G14 k3s ...`; write `ssh G14:k3s ...` so location and operation stay separated.",
|
||||
"If an ssh-like remote command fails with timeout/kex/exit-255 friction, stderr includes one low-noise UNIDESK_SSH_HINT JSON line with the argv retry command.",
|
||||
"Use -- before a remote command that intentionally starts with a dash.",
|
||||
],
|
||||
|
||||
+44
-31
@@ -39,7 +39,7 @@ export interface SshFailureHint {
|
||||
}
|
||||
|
||||
const argvQuotedSshSubcommands = new Set(["rg", "grep", "sed", "nl", "stat", "du", "ls", "cat", "head", "tail", "wc", "pwd"]);
|
||||
const d601NativeKubeconfig = "/etc/rancher/k3s/k3s.yaml";
|
||||
const nativeK3sKubeconfig = "/etc/rancher/k3s/k3s.yaml";
|
||||
const legacyK3sOperationRouteSegments = new Set([
|
||||
"guard",
|
||||
"kubectl",
|
||||
@@ -815,8 +815,7 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
|
||||
return { remoteCommand: shellArgv(["glob", ...args.slice(1)]), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
if (subcommand === "k3s") {
|
||||
if (args[1] === "apply-patch") return buildK3sApplyPatchCommand(args.slice(2));
|
||||
return { remoteCommand: buildK3sCommand(args.slice(1)), requiresStdin: false, invocationKind: "helper" };
|
||||
throw new Error("ssh k3s shorthand is unsupported; put k3s in the route, for example: ssh D601:k3s kubectl get nodes");
|
||||
}
|
||||
if (argvQuotedSshSubcommands.has(subcommand)) {
|
||||
return { remoteCommand: shellArgv(args), requiresStdin: false, invocationKind: "argv" };
|
||||
@@ -849,8 +848,13 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
|
||||
|
||||
export function parseSshInvocation(target: string, args: string[]): ParsedSshInvocation {
|
||||
const route = parseSshRoute(target);
|
||||
const parsed = route.plane === "k3s" ? parseK3sRouteArgs(route, args) : parseSshArgs(args);
|
||||
return { providerId: route.providerId, route, parsed };
|
||||
if (route.plane === "k3s") {
|
||||
return { providerId: route.providerId, route, parsed: parseK3sRouteArgs(route, args) };
|
||||
}
|
||||
if ((args[0] ?? "") === "k3s") {
|
||||
throw new Error(`ssh k3s shorthand is unsupported; use route syntax instead: ssh ${route.providerId}:k3s ${args.slice(1).join(" ")}`.trim());
|
||||
}
|
||||
return { providerId: route.providerId, route, parsed: parseSshArgs(args) };
|
||||
}
|
||||
|
||||
export function parseSshRoute(target: string): ParsedSshRoute {
|
||||
@@ -877,11 +881,12 @@ export function parseSshRoute(target: string): ParsedSshRoute {
|
||||
}
|
||||
|
||||
function k3sOperationInRouteMessage(target: string, operation: string, namespace: string | undefined, resource: string | undefined): string {
|
||||
if (operation === "kubectl") return `ssh k3s route must locate a target only; use "ssh D601:k3s kubectl ..." instead of "${target}"`;
|
||||
if (operation === "script" && namespace === undefined) return `ssh k3s route must locate a target only; use "ssh D601:k3s script <<'SCRIPT'" instead of "${target}"`;
|
||||
if (operation === "guard") return `ssh k3s route must locate a target only; use "ssh D601:k3s guard" or "ssh D601:k3s" instead of "${target}"`;
|
||||
if (namespace !== undefined && resource !== undefined) return `ssh k3s route must locate a target only; use "ssh D601:k3s:${namespace}:${resource} ${operation} ..." instead of "${target}"`;
|
||||
return `ssh k3s route must locate a target only; put operation "${operation}" after the route, for example "ssh D601:k3s ${operation} ..."`;
|
||||
const providerId = target.split(":")[0] || "<provider>";
|
||||
if (operation === "kubectl") return `ssh k3s route must locate a target only; use "ssh ${providerId}:k3s kubectl ..." instead of "${target}"`;
|
||||
if (operation === "script" && namespace === undefined) return `ssh k3s route must locate a target only; use "ssh ${providerId}:k3s script <<'SCRIPT'" instead of "${target}"`;
|
||||
if (operation === "guard") return `ssh k3s route must locate a target only; use "ssh ${providerId}:k3s guard" or "ssh ${providerId}:k3s" instead of "${target}"`;
|
||||
if (namespace !== undefined && resource !== undefined) return `ssh k3s route must locate a target only; use "ssh ${providerId}:k3s:${namespace}:${resource} ${operation} ..." instead of "${target}"`;
|
||||
return `ssh k3s route must locate a target only; put operation "${operation}" after the route, for example "ssh ${providerId}:k3s ${operation} ..."`;
|
||||
}
|
||||
|
||||
function shellArgv(args: string[]): string {
|
||||
@@ -990,7 +995,7 @@ function buildFindCommand(args: string[]): string {
|
||||
|
||||
function parseK3sRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
|
||||
if (route.entry === null && route.namespace === null && route.resource === null) {
|
||||
return parseK3sControlPlaneOperation(args);
|
||||
return parseK3sControlPlaneOperation(route, args);
|
||||
}
|
||||
if (route.namespace === null || route.resource === null) {
|
||||
throw new Error("ssh k3s target route requires provider:k3s:<namespace>:<deployment|pod/resource>");
|
||||
@@ -998,19 +1003,19 @@ function parseK3sRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
|
||||
return parseK3sTargetOperation(route, args);
|
||||
}
|
||||
|
||||
function parseK3sControlPlaneOperation(args: string[]): ParsedSshArgs {
|
||||
function parseK3sControlPlaneOperation(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
|
||||
const operation = args[0] ?? "guard";
|
||||
if (operation === "apply-patch" || operation === "patch") {
|
||||
throw new Error("ssh D601:k3s apply-patch requires a workload route: ssh D601:k3s:<namespace>:<workload> apply-patch");
|
||||
throw new Error(`ssh ${route.providerId}:k3s apply-patch requires a workload route: ssh ${route.providerId}:k3s:<namespace>:<workload> apply-patch`);
|
||||
}
|
||||
if (operation === "script" || operation === "sh") {
|
||||
return { remoteCommand: buildK3sScriptCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
|
||||
}
|
||||
if (operation === "guard") {
|
||||
if (args.length > 1) throw new Error("ssh D601:k3s guard does not accept extra arguments");
|
||||
return { remoteCommand: buildD601K3sGuardCommand(), requiresStdin: false, invocationKind: "helper" };
|
||||
if (args.length > 1) throw new Error(`ssh ${route.providerId}:k3s guard does not accept extra arguments`);
|
||||
return { remoteCommand: buildK3sGuardCommand(route.providerId), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
return { remoteCommand: buildK3sCommand(args), requiresStdin: false, invocationKind: "helper" };
|
||||
return { remoteCommand: buildK3sCommand(route.providerId, args), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
|
||||
function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
|
||||
@@ -1030,7 +1035,7 @@ function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedS
|
||||
if (operation === "get" || operation === "describe") {
|
||||
return { remoteCommand: buildK3sTargetObjectCommand(operation, route, operationArgs), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
if (operation === "kubectl") throw new Error("ssh k3s kubectl is a control-plane operation; use ssh D601:k3s kubectl ...");
|
||||
if (operation === "kubectl") throw new Error(`ssh k3s kubectl is a control-plane operation; use ssh ${route.providerId}:k3s kubectl ...`);
|
||||
if (operation === "exec") {
|
||||
return { remoteCommand: buildK3sExecCommand([...targetArgs, ...k3sRouteCommandArgs(operationArgs)]), requiresStdin: false, invocationKind: "helper" };
|
||||
}
|
||||
@@ -1040,7 +1045,7 @@ function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedS
|
||||
function buildK3sTargetObjectCommand(action: "get" | "describe", route: ParsedSshRoute, args: string[]): string {
|
||||
if (route.namespace === null || route.resource === null) throw new Error(`ssh k3s ${action} target requires namespace and workload route`);
|
||||
if (args.includes("--follow") || args.includes("-f")) throw new Error(`ssh k3s target ${action} does not support follow mode`);
|
||||
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", action, "-n", route.namespace, normalizeK3sRouteResource(route.resource), ...args]);
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", action, "-n", route.namespace, normalizeK3sRouteResource(route.resource), ...args]);
|
||||
}
|
||||
|
||||
function k3sRouteTargetArgs(route: ParsedSshRoute): string[] {
|
||||
@@ -1058,37 +1063,45 @@ function k3sRouteCommandArgs(args: string[]): string[] {
|
||||
return args[0] === "--" ? args : ["--", ...args];
|
||||
}
|
||||
|
||||
function buildK3sCommand(args: string[]): string {
|
||||
function buildK3sCommand(providerId: string, args: string[]): string {
|
||||
const action = args[0] ?? "";
|
||||
if (action.length === 0 || action === "--help" || action === "-h" || action === "help") {
|
||||
throw new Error("ssh k3s requires a subcommand: guard, kubectl, get, describe, logs or exec");
|
||||
}
|
||||
if (action === "guard") return buildD601K3sGuardCommand();
|
||||
if (action === "guard") return buildK3sGuardCommand(providerId);
|
||||
if (action === "exec") return buildK3sExecCommand(args.slice(1));
|
||||
if (action === "script") return buildK3sScriptCommand(args.slice(1));
|
||||
if (action === "logs") return buildK3sLogsCommand(args.slice(1));
|
||||
if (action === "kubectl") {
|
||||
const kubectlArgs = args.slice(1);
|
||||
if (kubectlArgs.length === 0) throw new Error("ssh k3s kubectl requires kubectl arguments");
|
||||
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
}
|
||||
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...args]);
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...args]);
|
||||
}
|
||||
|
||||
function buildD601K3sGuardCommand(): string {
|
||||
function buildK3sGuardCommand(providerId: string): string {
|
||||
const provider = providerId.toUpperCase();
|
||||
const providerNodeCheck = provider === "D601"
|
||||
? "printf '%s\\n' \"$nodes\" | grep -Fx d601 >/dev/null || { printf 'native_k3s_guard=blocked provider=%s reason=d601-node-missing\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }"
|
||||
: provider === "G14"
|
||||
? "printf '%s\\n' \"$nodes\" | grep -Fx ubuntu-rog-zephyrus-g14-ga401iv-ga401iv >/dev/null || { printf 'native_k3s_guard=blocked provider=%s reason=g14-node-missing\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }"
|
||||
: "[ -n \"$nodes\" ] || { printf 'native_k3s_guard=blocked provider=%s reason=node-list-empty\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }";
|
||||
const script = [
|
||||
"set -euo pipefail",
|
||||
`export KUBECONFIG=${shellQuote(d601NativeKubeconfig)}`,
|
||||
`export KUBECONFIG=${shellQuote(nativeK3sKubeconfig)}`,
|
||||
`export UNIDESK_K3S_PROVIDER_ID=${shellQuote(providerId)}`,
|
||||
"context=$(kubectl config current-context)",
|
||||
"server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')",
|
||||
"nodes=$(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{\"\\n\"}{end}')",
|
||||
"printf 'kubeconfig=%s\\n' \"$KUBECONFIG\"",
|
||||
"printf 'provider=%s\\n' \"$UNIDESK_K3S_PROVIDER_ID\"",
|
||||
"printf 'context=%s\\n' \"$context\"",
|
||||
"printf 'server=%s\\n' \"$server\"",
|
||||
"printf 'nodes=%s\\n' \"$(printf '%s' \"$nodes\" | tr '\\n' ' ')\"",
|
||||
"printf '%s\\n' \"$nodes\" | grep -Fx d601 >/dev/null || { printf 'native_k3s_guard=blocked reason=d601-node-missing\\n' >&2; exit 1; }",
|
||||
providerNodeCheck,
|
||||
"case \"$server\" in *127.0.0.1:11700*|*docker-desktop*) printf 'native_k3s_guard=blocked reason=docker-desktop-context server=%s\\n' \"$server\" >&2; exit 1;; esac",
|
||||
"printf 'native_k3s_guard=ok\\n'",
|
||||
"printf 'native_k3s_guard=ok provider=%s\\n' \"$UNIDESK_K3S_PROVIDER_ID\"",
|
||||
].join("; ");
|
||||
return shellArgv(["bash", "-c", script]);
|
||||
}
|
||||
@@ -1125,7 +1138,7 @@ function buildK3sExecCommand(args: string[]): string {
|
||||
"--",
|
||||
...parsed.command,
|
||||
];
|
||||
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
}
|
||||
|
||||
function buildK3sScriptCommand(args: string[]): string {
|
||||
@@ -1148,7 +1161,7 @@ function buildK3sScriptCommand(args: string[]): string {
|
||||
"--",
|
||||
...parsed.command,
|
||||
];
|
||||
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
}
|
||||
|
||||
function buildK3sApplyPatchCommand(args: string[]): ParsedSshArgs {
|
||||
@@ -1173,7 +1186,7 @@ function buildK3sApplyPatchCommand(args: string[]): ParsedSshArgs {
|
||||
];
|
||||
const wrapper = podApplyPatchStdinWrapper();
|
||||
return {
|
||||
remoteCommand: shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]),
|
||||
remoteCommand: shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]),
|
||||
requiresStdin: true,
|
||||
invocationKind: "helper",
|
||||
stdinPrefix: wrapper.prefix,
|
||||
@@ -1187,7 +1200,7 @@ function buildK3sHostScriptCommand(parsed: K3sTargetOptions): string {
|
||||
if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container");
|
||||
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s script without a workload does not accept kubectl log options");
|
||||
const shell = parsed.shell ?? "sh";
|
||||
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, shell, "-s", "--", ...parsed.command]);
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, shell, "-s", "--", ...parsed.command]);
|
||||
}
|
||||
|
||||
function buildK3sLogsCommand(args: string[]): string {
|
||||
@@ -1202,7 +1215,7 @@ function buildK3sLogsCommand(args: string[]): string {
|
||||
...(parsed.container === null ? [] : ["-c", parsed.container]),
|
||||
...parsed.kubectlOptions,
|
||||
];
|
||||
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
|
||||
}
|
||||
|
||||
function parseK3sTargetOptions(args: string[], commandName: string, options: ParseK3sTargetOptionsOptions): K3sTargetOptions {
|
||||
|
||||
@@ -35,11 +35,11 @@ export function runSshArgvGuidanceContract(): JsonRecord {
|
||||
assertCondition(script.remoteCommand === "'bash' '-s' '--' 'alpha beta'", "script helper must pass stdin to shell directly", script);
|
||||
assertCondition(script.requiresStdin === true, "script helper must require stdin", script);
|
||||
|
||||
const k3sGuard = parseSshArgs(["k3s", "guard"]);
|
||||
const k3sGuard = parseSshInvocation("D601:k3s", ["guard"]).parsed;
|
||||
assertCondition(k3sGuard.invocationKind === "helper", "k3s guard must be classified as helper", k3sGuard);
|
||||
assertCondition(k3sGuard.remoteCommand?.includes("KUBECONFIG") && k3sGuard.remoteCommand.includes("/etc/rancher/k3s/k3s.yaml"), "k3s guard must force native k3s kubeconfig", k3sGuard);
|
||||
|
||||
const k3sExec = parseSshArgs(["k3s", "exec", "--namespace", "hwlab-dev", "--deployment", "hwlab-cloud-api", "--", "node", "-e", "console.log(process.version)"]);
|
||||
const k3sExec = parseSshInvocation("D601:k3s", ["exec", "--namespace", "hwlab-dev", "--deployment", "hwlab-cloud-api", "--", "node", "-e", "console.log(process.version)"]).parsed;
|
||||
assertCondition(k3sExec.invocationKind === "helper", "k3s exec must be classified as helper", k3sExec);
|
||||
assertCondition(k3sExec.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'node' '-e' 'console.log(process.version)'", "k3s exec must assemble kubectl argv without nested shell quoting", k3sExec);
|
||||
|
||||
@@ -48,6 +48,16 @@ export function runSshArgvGuidanceContract(): JsonRecord {
|
||||
assertCondition(routeKubectl.route.plane === "k3s" && routeKubectl.route.entry === null, "route must keep kubectl as an operation, not as a route entry", routeKubectl);
|
||||
assertCondition(routeKubectl.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'get' 'pods' '-n' 'hwlab-dev'", "D601:k3s kubectl must map to kubectl argv", routeKubectl);
|
||||
|
||||
const g14Guard = parseSshInvocation("G14:k3s", []);
|
||||
assertCondition(g14Guard.providerId === "G14" && g14Guard.route.plane === "k3s", "G14:k3s must parse as a native k3s route", g14Guard);
|
||||
assertCondition(g14Guard.parsed.remoteCommand?.includes("UNIDESK_K3S_PROVIDER_ID") && g14Guard.parsed.remoteCommand.includes("ubuntu-rog-zephyrus-g14-ga401iv-ga401iv"), "G14:k3s guard must use the G14 node profile", g14Guard);
|
||||
|
||||
assertThrows(
|
||||
() => parseSshInvocation("G14", ["k3s", "kubectl", "get", "nodes"]),
|
||||
/unsupported.*ssh G14:k3s/u,
|
||||
"k3s must be a route plane, not a post-provider shorthand",
|
||||
);
|
||||
|
||||
const routeTarget = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["node", "-e", "console.log(process.version)"]);
|
||||
assertCondition(routeTarget.route.namespace === "hwlab-dev" && routeTarget.route.resource === "hwlab-cloud-api", "route target must parse namespace and workload", routeTarget);
|
||||
assertCondition(routeTarget.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'node' '-e' 'console.log(process.version)'", "D601:k3s:<namespace>:<workload> must default to deployment exec", routeTarget);
|
||||
@@ -96,6 +106,7 @@ export function runSshArgvGuidanceContract(): JsonRecord {
|
||||
const helpText = JSON.stringify(sshHelp());
|
||||
assertCondition(helpText.includes("ssh <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT'"), "ssh help must recommend stdin script passthrough for shell scripts", helpText);
|
||||
assertCondition(helpText.includes("ssh D601:k3s kubectl get pods -n hwlab-dev"), "ssh help must document k3s kubectl operation", helpText);
|
||||
assertCondition(helpText.includes("ssh G14:k3s kubectl get pipelineruns -n hwlab-ci"), "ssh help must document G14 k3s route operation", helpText);
|
||||
assertCondition(helpText.includes("ssh D601:k3s script <<'SCRIPT'"), "ssh help must document k3s control-plane script operation", helpText);
|
||||
assertCondition(helpText.includes("ssh D601:k3s:hwlab-dev:hwlab-cloud-api apply-patch <<'PATCH'"), "ssh help must document k3s pod apply-patch operation", helpText);
|
||||
assertCondition(helpText.includes("ssh D601:k3s:hwlab-dev:hwlab-cloud-api script <<'SCRIPT'"), "ssh help must document k3s script operation", helpText);
|
||||
@@ -111,6 +122,7 @@ export function runSshArgvGuidanceContract(): JsonRecord {
|
||||
"stdin script form removes shell-command strings for host and k3s workload scripts",
|
||||
"pod apply-patch operation injects helper and forwards patch stdin",
|
||||
"legacy operation-in-route forms are rejected with canonical route-plus-operation guidance",
|
||||
"post-provider k3s shorthand is rejected so location and operation stay separated",
|
||||
"k3s route stays location-only while operations fix native kubeconfig and assemble kubectl exec as argv",
|
||||
"ssh-like timeout/kex failures emit one structured argv retry hint",
|
||||
"help text documents stdin script passthrough and UNIDESK_SSH_HINT",
|
||||
|
||||
Reference in New Issue
Block a user