Merge pull request #2700 from pikasTech/feat/hwpod-temporal-split
HWPOD 独立 L1 运行面与 D601-WIN 恢复配置
This commit is contained in:
@@ -24,6 +24,54 @@ cloud:
|
||||
sourceKey: HWLAB_API_KEY
|
||||
|
||||
targets:
|
||||
D601-WIN:
|
||||
enabled: true
|
||||
lane: v03
|
||||
providerId: D601-WIN
|
||||
windowsRoute: D601-WIN:win/f/Work/ConStart
|
||||
nodeId: node-d601-f103-v2
|
||||
hwpodId: constart-71freq-c
|
||||
interactiveUser: liang
|
||||
python:
|
||||
probeLauncherPath: "C:\\Windows\\py.exe"
|
||||
guiLauncherPath: "C:\\Windows\\pyw.exe"
|
||||
launcherArgs: []
|
||||
expectedVersionPrefix: "3.13.14"
|
||||
runtime:
|
||||
root: "C:\\Users\\liang\\hwlab-node"
|
||||
scriptPath: "C:\\Users\\liang\\hwlab-node\\hwlab-node.py"
|
||||
configPath: "C:\\Users\\liang\\.hwlab\\config.json"
|
||||
credentialPath: "C:\\Users\\liang\\.hwlab\\hwpod-node.token"
|
||||
logPath: "C:\\Users\\liang\\.hwlab\\logs\\hwlab-node.log"
|
||||
artifact:
|
||||
metadataUrl: https://lab-dev.hwpod.com/v1/hwlab-node/update
|
||||
channel: stable
|
||||
platform: windows
|
||||
workspace:
|
||||
allowedRoots:
|
||||
- "F:\\Work\\ConStart"
|
||||
defaultRoot: "F:\\Work\\ConStart"
|
||||
desktopConfig:
|
||||
serverUrl: https://lab-dev.hwpod.com
|
||||
autoConnect: true
|
||||
requireCredential: true
|
||||
updateEnabled: true
|
||||
updateAutoApplyWhenIdle: false
|
||||
startup:
|
||||
mode: hkcu-run
|
||||
runKeyName: hwlab-node-d601-win
|
||||
initialLaunch: interactive-process
|
||||
processPattern: hwlab-node.py
|
||||
legacyTaskNames:
|
||||
- HWLAB-HWPOD-Node-WS
|
||||
- hwpod-node-ws-periodic
|
||||
legacyRunKeyNames:
|
||||
- hwpod-node-ws
|
||||
legacyProcessPatterns:
|
||||
- tools\hwpod-node.ts connect
|
||||
- run-node-ws-detached.cmd
|
||||
projectManagementSourceRef: config/hwlab-project-management/constart-71freq-mdtodo.yaml#projectManagement.sources[0]
|
||||
|
||||
G14-WSL:
|
||||
enabled: true
|
||||
lane: v03
|
||||
@@ -64,4 +112,3 @@ targets:
|
||||
initialLaunch: interactive-process
|
||||
processPattern: hwlab-node.py
|
||||
projectManagementSourceRef: config/hwlab-project-management/constar-workspace-mdtodo.yaml#projectManagement.sources[0]
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ lanes:
|
||||
- hwlab-project-management
|
||||
- hwlab-harnessrl-api
|
||||
- hwlab-harnessrl-worker
|
||||
- hwlab-hwpod-api
|
||||
- hwlab-hwpod-worker
|
||||
- hwlab-cloud-web
|
||||
- hwlab-gateway
|
||||
- hwlab-edge-proxy
|
||||
@@ -209,6 +211,39 @@ lanes:
|
||||
traceTimeline:
|
||||
autoExpandRunning: false
|
||||
autoCollapseTerminal: false
|
||||
hwpod:
|
||||
runtimeApiUrl: https://lab-dev.hwpod.com
|
||||
runtimeApiAuth:
|
||||
sourceRef: hwlab/nc01-v03-admin.env
|
||||
sourceKey: HWLAB_API_KEY
|
||||
publicExposure:
|
||||
publicBaseUrl: https://lab-dev-hwpod.hwpod.com
|
||||
hostname: lab-dev-hwpod.hwpod.com
|
||||
expectedA: 152.53.229.148
|
||||
upstream: http://127.0.0.1:5175
|
||||
healthPath: /health/live
|
||||
stateDir: .state/hwpod-native/services
|
||||
temporal:
|
||||
address: 10.43.145.243:7233
|
||||
namespace: unidesk
|
||||
taskQueue: hwlab-v03-hwpod-native-l1
|
||||
activity:
|
||||
startToCloseTimeoutMs: 900000
|
||||
retryInitialIntervalMs: 1000
|
||||
retryMaximumIntervalMs: 10000
|
||||
retryMaximumAttempts: 1
|
||||
api:
|
||||
bindHost: 0.0.0.0
|
||||
probeHost: 127.0.0.1
|
||||
port: 6681
|
||||
worker:
|
||||
bindHost: 127.0.0.1
|
||||
probeHost: 127.0.0.1
|
||||
healthPort: 6682
|
||||
web:
|
||||
bindHost: 0.0.0.0
|
||||
probeHost: 127.0.0.1
|
||||
port: 5175
|
||||
caserun:
|
||||
publicExposure:
|
||||
publicBaseUrl: https://lab-dev-caserun.hwpod.com
|
||||
@@ -225,6 +260,12 @@ lanes:
|
||||
bindHost: 0.0.0.0
|
||||
probeHost: 127.0.0.1
|
||||
port: 5173
|
||||
hwpodPreinstall:
|
||||
enabled: true
|
||||
configRefs:
|
||||
preinstall: config/hwlab-hwpod-preinstalls/constart-71freq-c.yaml#hwpodPreinstall
|
||||
projectManagementSource: config/hwlab-project-management/constart-71freq-mdtodo.yaml#projectManagement.sources[0]
|
||||
gatewayProfile: config/hwlab-gateway/constart-71freq.yaml#gateway.profile
|
||||
sourceAuthority:
|
||||
extends: templates.hwlabV03.sourceAuthorityGiteaSnapshot
|
||||
giteaMirrorRepoKey: hwlab-nc01-v03
|
||||
|
||||
@@ -93,6 +93,9 @@ targets:
|
||||
- id: hwlab-workbench-native
|
||||
configRef: config/hwlab-node-lanes.yaml
|
||||
path: lanes.v03.targets.NC01.nativeDevelopment.workbench.publicExposure
|
||||
- id: hwlab-hwpod-native
|
||||
configRef: config/hwlab-node-lanes.yaml
|
||||
path: lanes.v03.targets.NC01.nativeDevelopment.hwpod.publicExposure
|
||||
- id: agentrun-native
|
||||
configRef: config/agentrun.yaml
|
||||
path: nativeDevelopment.manager.publicExposure
|
||||
|
||||
@@ -178,3 +178,51 @@ HWPOD服务应提供有界 typed topology read model,统一表达 owning 配
|
||||
该投影必须分别表达 service ready、node online、HWPOD available、leased/busy 和 capability mismatch。列表接口只返回有界摘要并支持服务端筛选、排序和 cursor;完整 spec document、日志和诊断通过实体详情或专用查询渐进披露。
|
||||
|
||||
Cloud API 可以代理并映射主体、ACL、错误与 correlation,但不得在浏览器或 Cloud API 临时拼装第二份 topology authority。若连接规模要求抽取独立 HWPOD 服务,registry、topology、operation 和诊断事实必须随 owner 一次迁移。
|
||||
|
||||
## 7. 独立服务架构与运行等级
|
||||
|
||||
HWPOD 服务应作为独立的可组合应用交付,分为 Temporal worker、API 后端和 Web 前端三个可独立启动、停止、检查和扩缩的运行单元。
|
||||
|
||||
### 7.1 目标架构
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
W[HWPOD Web] --> A[HWPOD API]
|
||||
C[HWPOD CLI / Cloud API] --> A
|
||||
A --> T[Temporal namespace unidesk]
|
||||
T --> WK[HWPOD Worker]
|
||||
WK --> R[Registry / Operation ledger]
|
||||
WK --> N[主动出站 HWPOD Node]
|
||||
N --> H[真实硬件与工作区]
|
||||
```
|
||||
|
||||
- API 只负责 HTTP envelope、鉴权、correlation、查询投影和 workflow 提交。
|
||||
- Worker 负责 HWPOD workflow、节点路由、operation 状态推进和结果归属。
|
||||
- Web 只依赖 HWPOD API 合同,不复制 registry、topology 或 operation authority。
|
||||
- Cloud API 可以代理 HWPOD API,但不得在 Cloud API 内保留第二份 HWPOD 业务 authority。
|
||||
- HWPOD Node 通过主动出站连接注册到服务端入口;节点不得依赖用户侧公网入站端口。
|
||||
|
||||
### 7.2 L0/L1 合同
|
||||
|
||||
- L0 必须能在不启动 API、worker、Temporal、Web 或 Kubernetes workload 的情况下,直接验证 spec 解析、topology 投影、workflow 输入和错误分类。
|
||||
- L1 必须能按 owning YAML 独立启动 API、worker 和 Web,并用同一 HWPOD API 合同完成健康、spec、topology、operation 和 workflow smoke。
|
||||
- L1 Web 的首选入口为 owning YAML 声明的 HTTPS origin;公共入口不可用时,只能使用同一 YAML 声明的固定公网 HTTP 降级入口。
|
||||
- L1 API、worker 和 Web 必须分别暴露健康/就绪检查;worker 不得创建公网业务入口。
|
||||
- L1 Web 必须支持 HWPOD 资源列表、节点状态、operation 状态和明确 blocker 展示,且页面刷新后仍通过 API 恢复同一投影。
|
||||
|
||||
### 7.3 稳定接口
|
||||
|
||||
- `GET /health/live` 和 `GET /health/ready`:分别返回进程存活和 HWPOD/Temporal 依赖就绪状态。
|
||||
- `GET /v1/hwpod/specs`:返回有界 HWPOD spec 摘要和可用性。
|
||||
- `GET /v1/hwpod/topology`:返回 node、HWPOD、能力、占用、最近 operation 和 blocker 投影。
|
||||
- `POST /v1/hwpod/operations`:提交带有 `operationId`、`hwpodId`、`nodeId`、调用方和租约摘要的 workflow。
|
||||
- `GET /v1/hwpod/operations/{operationId}`:读取同一 operation 的 durable 状态和结果摘要。
|
||||
|
||||
所有接口必须保持 `valuesPrinted=false`,不得返回 token、Authorization、Secret 值或可复用节点凭据。
|
||||
|
||||
### 7.4 验收契约
|
||||
|
||||
- L0:spec、topology、workflow contract 和 typed error 测试通过;默认路径不产生外部 mutation。
|
||||
- L1:API、worker、Web 可分别重启;worker 重启后未完成 workflow 可继续;Web 通过固定入口完成首屏、资源列表和 operation 状态交互。
|
||||
- L1:D601 节点注册后,HWPOD topology 同时显示 node online、spec available 和 capability match;节点断开后显示 node offline,不得伪造硬件成功。
|
||||
- L1:Cloud API 代理、独立 HWPOD API 和 CLI 对同一 operation 返回相同的 operation identity、状态和 blocker code。
|
||||
|
||||
@@ -73,7 +73,15 @@ interface HwpodNodeSpec {
|
||||
updateEnabled: boolean;
|
||||
updateAutoApplyWhenIdle: boolean;
|
||||
};
|
||||
startup: { mode: string; runKeyName: string; initialLaunch: string; processPattern: string };
|
||||
startup: {
|
||||
mode: string;
|
||||
runKeyName: string;
|
||||
initialLaunch: string;
|
||||
processPattern: string;
|
||||
legacyTaskNames: string[];
|
||||
legacyRunKeyNames: string[];
|
||||
legacyProcessPatterns: string[];
|
||||
};
|
||||
sourceRef: string;
|
||||
source: SourceSpec;
|
||||
}
|
||||
@@ -253,6 +261,9 @@ function readSpec(node: string, lane: string): HwpodNodeSpec {
|
||||
runKeyName: text(startup.runKeyName, `targets.${node}.startup.runKeyName`),
|
||||
initialLaunch: text(startup.initialLaunch, `targets.${node}.startup.initialLaunch`),
|
||||
processPattern: text(startup.processPattern, `targets.${node}.startup.processPattern`),
|
||||
legacyTaskNames: startup.legacyTaskNames === undefined ? [] : texts(startup.legacyTaskNames, `targets.${node}.startup.legacyTaskNames`),
|
||||
legacyRunKeyNames: startup.legacyRunKeyNames === undefined ? [] : texts(startup.legacyRunKeyNames, `targets.${node}.startup.legacyRunKeyNames`),
|
||||
legacyProcessPatterns: startup.legacyProcessPatterns === undefined ? [] : texts(startup.legacyProcessPatterns, `targets.${node}.startup.legacyProcessPatterns`),
|
||||
},
|
||||
sourceRef,
|
||||
source,
|
||||
@@ -285,7 +296,7 @@ function validateCrossRefs(spec: HwpodNodeSpec): void {
|
||||
if (!spec.workspace.allowedRoots.includes(spec.workspace.defaultRoot)) conflicts.push("workspace.defaultRoot 不在 allowedRoots 中");
|
||||
if (spec.source.workspaceRootRef !== spec.workspace.defaultRoot) conflicts.push("source.workspaceRootRef 与 workspace.defaultRoot 不一致");
|
||||
if (spec.startup.mode !== "hkcu-run") conflicts.push(`startup.mode 仅支持 hkcu-run,实际为 ${spec.startup.mode}`);
|
||||
if (!spec.windowsRoute.endsWith(":win")) conflicts.push("windowsRoute 必须指向 :win plane");
|
||||
if (!/:win(?:\/[a-zA-Z](?:\/.*)?)?$/u.test(spec.windowsRoute)) conflicts.push("windowsRoute 必须指向 :win plane,并可附带显式 Windows cwd");
|
||||
if (conflicts.length > 0) throw new Error(`HWPOD 节点配置冲突:${conflicts.join(";")}`);
|
||||
}
|
||||
|
||||
@@ -448,6 +459,9 @@ function applyDesktop(spec: HwpodNodeSpec, artifact: ArtifactBundle, credential:
|
||||
launcherArgs: spec.python.launcherArgs,
|
||||
expectedVersionPrefix: spec.python.expectedVersionPrefix,
|
||||
runKeyName: spec.startup.runKeyName,
|
||||
legacyTaskNames: spec.startup.legacyTaskNames,
|
||||
legacyRunKeyNames: spec.startup.legacyRunKeyNames,
|
||||
legacyProcessPatterns: spec.startup.legacyProcessPatterns,
|
||||
artifactBase64: artifact.content.toString("base64"),
|
||||
artifactSha256: artifact.sha256,
|
||||
configBase64: Buffer.from(`${JSON.stringify(config, null, 2)}\n`, "utf8").toString("base64"),
|
||||
@@ -570,11 +584,31 @@ $logPath = [string]$payload.logPath
|
||||
$probe = [string]$payload.probeLauncherPath
|
||||
$gui = [string]$payload.guiLauncherPath
|
||||
$launcherArgs = @($payload.launcherArgs | ForEach-Object { [string]$_ })
|
||||
$legacyTaskNames = @($payload.legacyTaskNames | ForEach-Object { [string]$_ })
|
||||
$legacyRunKeyNames = @($payload.legacyRunKeyNames | ForEach-Object { [string]$_ })
|
||||
$legacyProcessPatterns = @($payload.legacyProcessPatterns | ForEach-Object { [string]$_ })
|
||||
if (-not (Test-Path $probe)) { throw "Python probe launcher missing: $probe" }
|
||||
if (-not (Test-Path $gui)) { throw "Python GUI launcher missing: $gui" }
|
||||
$versionOut = @(& $probe @launcherArgs -c "import sys;import tkinter;print(sys.version);print(sys.executable);print(tkinter.TkVersion)" 2>&1)
|
||||
if ($LASTEXITCODE -ne 0 -or (($versionOut -join "\`n") -notlike "*$($payload.expectedVersionPrefix)*")) { throw "Python version mismatch: $($versionOut -join ' | ')" }
|
||||
New-Item -ItemType Directory -Force -Path $root, (Split-Path -Parent $configPath), (Split-Path -Parent $credentialPath), (Split-Path -Parent $logPath) | Out-Null
|
||||
$legacyStopped = @()
|
||||
foreach ($taskName in $legacyTaskNames) {
|
||||
Stop-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||
Disable-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
foreach ($runKey in $legacyRunKeyNames) {
|
||||
Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name $runKey -ErrorAction SilentlyContinue
|
||||
}
|
||||
$legacy = @(Get-CimInstance Win32_Process | Where-Object {
|
||||
$commandLine = [string]$_.CommandLine
|
||||
$legacyProcessPatterns | Where-Object { $commandLine.Contains($_) } | Select-Object -First 1
|
||||
})
|
||||
foreach ($proc in $legacy) {
|
||||
Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
$legacyStopped += [int]$proc.ProcessId
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
$old = @(Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and $_.CommandLine.Contains($scriptPath) })
|
||||
foreach ($proc in $old) { Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue }
|
||||
Start-Sleep -Milliseconds 500
|
||||
@@ -597,9 +631,15 @@ Start-Sleep -Seconds 3
|
||||
$processes = @(Get-CimInstance Win32_Process | Where-Object { $_.Name -like 'python*.exe' -and $_.CommandLine -and $_.CommandLine.Contains($scriptPath) })
|
||||
$explorerSessions = @(Get-Process explorer -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SessionId -Unique)
|
||||
$interactive = @($processes | Where-Object { $explorerSessions -contains $_.SessionId })
|
||||
$legacyTasksStillEnabled = @($legacyTaskNames | Where-Object { $task = Get-ScheduledTask -TaskName $_ -ErrorAction SilentlyContinue; $task -and $task.Settings.Enabled })
|
||||
$legacyProcessesRemaining = @(Get-CimInstance Win32_Process | Where-Object {
|
||||
$commandLine = [string]$_.CommandLine
|
||||
$legacyProcessPatterns | Where-Object { $commandLine.Contains($_) } | Select-Object -First 1
|
||||
})
|
||||
$ready = [bool](($processes.Count -eq 1) -and ($interactive.Count -eq 1) -and ($legacyTasksStillEnabled.Count -eq 0) -and ($legacyProcessesRemaining.Count -eq 0))
|
||||
$result = [ordered]@{
|
||||
ok = [bool](($processes.Count -eq 1) -and ($interactive.Count -eq 1))
|
||||
status = $(if (($processes.Count -eq 1) -and ($interactive.Count -eq 1)) { 'applied' } else { 'installed-not-interactive' })
|
||||
ok = $ready
|
||||
status = $(if ($ready) { 'applied' } elseif (($processes.Count -eq 1) -and ($interactive.Count -eq 1)) { 'legacy-authority-not-retired' } else { 'installed-not-interactive' })
|
||||
pythonSummary = (($versionOut | Select-Object -First 3) -join ' | ')
|
||||
scriptSha256 = 'sha256:' + $artifactActual
|
||||
configSha256 = 'sha256:' + $configActual
|
||||
@@ -609,6 +649,11 @@ $result = [ordered]@{
|
||||
processIds = @($processes | ForEach-Object { [int]$_.ProcessId })
|
||||
runKeyReady = $true
|
||||
stoppedProcessIds = @($old | ForEach-Object { [int]$_.ProcessId })
|
||||
retiredLegacyProcessIds = @($legacyStopped)
|
||||
retiredLegacyTaskNames = @($legacyTaskNames)
|
||||
retiredLegacyRunKeyNames = @($legacyRunKeyNames)
|
||||
legacyTasksStillEnabled = @($legacyTasksStillEnabled)
|
||||
legacyProcessIdsRemaining = @($legacyProcessesRemaining | ForEach-Object { [int]$_.ProcessId })
|
||||
valuesRedacted = $true
|
||||
}
|
||||
$result | ConvertTo-Json -Compress -Depth 6
|
||||
|
||||
@@ -4,7 +4,7 @@ import { runCommand } from "./command";
|
||||
import { readEnvSourceFile, requiredEnvValue } from "./secrets";
|
||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeNativeDevelopmentSpec } from "./hwlab-node-lanes";
|
||||
|
||||
type Application = "workbench" | "caserun";
|
||||
type Application = "workbench" | "hwpod" | "caserun";
|
||||
type Service = "api" | "worker" | "web";
|
||||
type Action = "start" | "stop" | "restart" | "status" | "logs";
|
||||
|
||||
@@ -15,6 +15,7 @@ export function hwlabNativeDevelopmentHelp(): Record<string, unknown> {
|
||||
usage: [
|
||||
"bun scripts/cli.ts hwlab nodes native-development workbench api|worker|web start|stop|restart|status|logs --node <node> --lane <lane>",
|
||||
"bun scripts/cli.ts hwlab nodes native-development workbench cli <health|session|turn|events ...> --node <node> --lane <lane>",
|
||||
"bun scripts/cli.ts hwlab nodes native-development hwpod api|worker|web start|stop|restart|status|logs --node <node> --lane <lane>",
|
||||
"bun scripts/cli.ts hwlab nodes native-development caserun api|web start|stop|restart|status|logs --node <node> --lane <lane>",
|
||||
],
|
||||
contract: "L1 API/Web 用户入口只输出 owning YAML 解析出的固定 HTTPS 域名;IP、port 和 bind/probe 地址不作为用户入口。",
|
||||
@@ -32,7 +33,11 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string,
|
||||
const spec = hwlabRuntimeLaneSpecForNode(lane, node);
|
||||
const native = spec.nativeDevelopment?.[application];
|
||||
if (native === undefined) throw new Error(`${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.${application} is required`);
|
||||
const env = application === "workbench" ? workbenchEnvironment(spec, native as HwlabRuntimeNativeDevelopmentSpec["workbench"]) : hwlabCaseRunNativeDevelopmentEnvironment(native as NonNullable<HwlabRuntimeNativeDevelopmentSpec["caserun"]>);
|
||||
const env = application === "workbench"
|
||||
? workbenchEnvironment(spec, native as HwlabRuntimeNativeDevelopmentSpec["workbench"])
|
||||
: application === "hwpod"
|
||||
? hwlabHwpodNativeDevelopmentEnvironment(native as NonNullable<HwlabRuntimeNativeDevelopmentSpec["hwpod"]>)
|
||||
: hwlabCaseRunNativeDevelopmentEnvironment(native as NonNullable<HwlabRuntimeNativeDevelopmentSpec["caserun"]>);
|
||||
if (application === "workbench" && serviceRaw === "cli") {
|
||||
const commandArgs = withoutOptions(args.slice(2), ["--node", "--lane"]);
|
||||
if (commandArgs.length === 0) throw new Error("native-development workbench cli requires a Workbench command");
|
||||
@@ -60,7 +65,7 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string,
|
||||
const publicEndpoint = service === "api"
|
||||
? { url: native.publicExposure.publicBaseUrl }
|
||||
: service === "web"
|
||||
? { url: `${native.publicExposure.publicBaseUrl}/${application === "workbench" ? "workbench" : "caserun"}` }
|
||||
? { url: application === "hwpod" ? native.publicExposure.publicBaseUrl : `${native.publicExposure.publicBaseUrl}/${application === "workbench" ? "workbench" : "caserun"}` }
|
||||
: null;
|
||||
return {
|
||||
ok: result.exitCode === 0 && payload.ok !== false,
|
||||
@@ -186,13 +191,13 @@ function parsePayload(stdout: string): Record<string, unknown> {
|
||||
}
|
||||
|
||||
function parseApplication(value: string | undefined): Application {
|
||||
if (value === "workbench" || value === "caserun") return value;
|
||||
throw new Error("native-development application must be workbench or caserun");
|
||||
if (value === "workbench" || value === "hwpod" || value === "caserun") return value;
|
||||
throw new Error("native-development application must be workbench, hwpod, or caserun");
|
||||
}
|
||||
|
||||
function parseService(value: string | undefined, application: Application): Service {
|
||||
if (value === "api" || value === "web" || (application === "workbench" && value === "worker")) return value;
|
||||
throw new Error(`native-development ${application} service must be ${application === "workbench" ? "api, worker, or web" : "api or web"}`);
|
||||
if (value === "api" || value === "web" || (application !== "caserun" && value === "worker")) return value;
|
||||
throw new Error(`native-development ${application} service must be ${application === "caserun" ? "api or web" : "api, worker, or web"}`);
|
||||
}
|
||||
|
||||
function parseAction(value: string | undefined): Action {
|
||||
@@ -236,3 +241,34 @@ function hwlabCaseRunNativeDevelopmentEnvironment(native: NonNullable<HwlabRunti
|
||||
CASERUN_WEB_PORT: String(native.web.port),
|
||||
};
|
||||
}
|
||||
|
||||
function hwlabHwpodNativeDevelopmentEnvironment(native: NonNullable<HwlabRuntimeNativeDevelopmentSpec["hwpod"]>): NodeJS.ProcessEnv {
|
||||
const authorizationSource = readEnvSourceFile({
|
||||
root: "/root/.unidesk/.state/secrets",
|
||||
sourceRef: native.runtimeApiAuth.sourceRef,
|
||||
});
|
||||
const runtimeApiKey = requiredEnvValue(authorizationSource.values, native.runtimeApiAuth.sourceKey, native.runtimeApiAuth.sourceRef);
|
||||
return {
|
||||
HWPOD_NATIVE_SERVICE_STATE_DIR: native.stateDir,
|
||||
HWPOD_PUBLIC_BASE_URL: native.publicExposure.publicBaseUrl,
|
||||
HWPOD_TEMPORAL_ADDRESS: native.temporal.address,
|
||||
HWPOD_TEMPORAL_NAMESPACE: native.temporal.namespace,
|
||||
HWPOD_TEMPORAL_TASK_QUEUE: native.temporal.taskQueue,
|
||||
HWPOD_ACTIVITY_START_TO_CLOSE_TIMEOUT_MS: String(native.temporal.activity.startToCloseTimeoutMs),
|
||||
HWPOD_ACTIVITY_RETRY_INITIAL_INTERVAL_MS: String(native.temporal.activity.retryInitialIntervalMs),
|
||||
HWPOD_ACTIVITY_RETRY_MAXIMUM_INTERVAL_MS: String(native.temporal.activity.retryMaximumIntervalMs),
|
||||
HWPOD_ACTIVITY_RETRY_MAXIMUM_ATTEMPTS: String(native.temporal.activity.retryMaximumAttempts),
|
||||
HWPOD_RUNTIME_API_URL: native.runtimeApiUrl,
|
||||
HWPOD_RUNTIME_API_AUTHORIZATION: `Bearer ${runtimeApiKey}`,
|
||||
HWPOD_API_BIND_HOST: native.api.bindHost,
|
||||
HWPOD_API_PROBE_HOST: native.api.probeHost,
|
||||
HWPOD_API_PORT: String(native.api.port),
|
||||
HWPOD_WORKER_BIND_HOST: native.worker.bindHost,
|
||||
HWPOD_WORKER_PROBE_HOST: native.worker.probeHost,
|
||||
HWPOD_WORKER_HEALTH_PORT: String(native.worker.healthPort),
|
||||
HWPOD_WEB_BIND_HOST: native.web.bindHost,
|
||||
HWPOD_WEB_PROBE_HOST: native.web.probeHost,
|
||||
HWPOD_WEB_PORT: String(native.web.port),
|
||||
HWPOD_NATIVE_API_URL: `http://${native.api.probeHost}:${native.api.port}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -502,7 +502,6 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
|
||||
const conflicts: string[] = [];
|
||||
|
||||
if (preinstall !== null) {
|
||||
requireEquals(conflicts, byKey.get("preinstall"), "nodeBinding.hwlabNode", spec.nodeId, `selected node ${spec.nodeId}`);
|
||||
requireEquals(conflicts, byKey.get("preinstall"), "nodeBinding.lane", spec.lane, `selected lane ${spec.lane}`);
|
||||
requireEquals(conflicts, byKey.get("preinstall"), "runtimeMount.namespace", spec.runtimeNamespace, `selected namespace ${spec.runtimeNamespace}`);
|
||||
requireSelfEquals(conflicts, byKey.get("preinstall"), "hwpodId", "specDocument.metadata.name");
|
||||
@@ -514,6 +513,7 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
|
||||
|
||||
const hwpodId = stringAt(preinstall, "hwpodId");
|
||||
const nodeId = stringAt(preinstall, "nodeBinding.nodeId");
|
||||
const hwlabNode = stringAt(preinstall, "nodeBinding.hwlabNode");
|
||||
const workspaceRootRef = stringAt(preinstall, "workspaceRootRef");
|
||||
if (pm !== null) {
|
||||
requireEquals(conflicts, byKey.get("projectManagementSource"), "sourceKind", "hwpod-workspace", "required sourceKind hwpod-workspace");
|
||||
@@ -522,7 +522,7 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
|
||||
if (workspaceRootRef !== null) requireEquals(conflicts, byKey.get("projectManagementSource"), "workspaceRootRef", workspaceRootRef, "preinstall workspaceRootRef");
|
||||
}
|
||||
if (gateway !== null) {
|
||||
requireEquals(conflicts, byKey.get("gatewayProfile"), "node", spec.nodeId, `selected node ${spec.nodeId}`);
|
||||
if (hwlabNode !== null) requireEquals(conflicts, byKey.get("gatewayProfile"), "node", hwlabNode, `preinstall hardware node ${hwlabNode}`);
|
||||
requireEquals(conflicts, byKey.get("gatewayProfile"), "lane", spec.lane, `selected lane ${spec.lane}`);
|
||||
if (hwpodId !== null) {
|
||||
requireEquals(conflicts, byKey.get("gatewayProfile"), "hwpodId", hwpodId, `preinstall hwpodId ${hwpodId}`);
|
||||
|
||||
@@ -736,6 +736,26 @@ export interface HwlabRuntimeNativeDevelopmentSpec {
|
||||
};
|
||||
};
|
||||
};
|
||||
readonly hwpod?: {
|
||||
readonly publicExposure: HwlabRuntimeNativePublicExposureSpec;
|
||||
readonly stateDir: string;
|
||||
readonly runtimeApiUrl: string;
|
||||
readonly runtimeApiAuth: { readonly sourceRef: string; readonly sourceKey: string };
|
||||
readonly temporal: {
|
||||
readonly address: string;
|
||||
readonly namespace: string;
|
||||
readonly taskQueue: string;
|
||||
readonly activity: {
|
||||
readonly startToCloseTimeoutMs: number;
|
||||
readonly retryInitialIntervalMs: number;
|
||||
readonly retryMaximumIntervalMs: number;
|
||||
readonly retryMaximumAttempts: number;
|
||||
};
|
||||
};
|
||||
readonly api: { readonly bindHost: string; readonly probeHost: string; readonly port: number };
|
||||
readonly worker: { readonly bindHost: string; readonly probeHost: string; readonly healthPort: number };
|
||||
readonly web: { readonly bindHost: string; readonly probeHost: string; readonly port: number };
|
||||
};
|
||||
readonly caserun?: {
|
||||
readonly publicExposure: HwlabRuntimeNativePublicExposureSpec;
|
||||
readonly stateDir: string;
|
||||
@@ -1594,10 +1614,44 @@ function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNati
|
||||
},
|
||||
},
|
||||
},
|
||||
...(raw.hwpod === undefined ? {} : { hwpod: nativeHwpodConfig(raw.hwpod, `${path}.hwpod`) }),
|
||||
...(raw.caserun === undefined ? {} : { caserun: nativeCaseRunConfig(raw.caserun, `${path}.caserun`) }),
|
||||
};
|
||||
}
|
||||
|
||||
function nativeHwpodConfig(value: unknown, path: string): NonNullable<HwlabRuntimeNativeDevelopmentSpec["hwpod"]> {
|
||||
const raw = asRecord(value, path);
|
||||
const runtimeApiAuth = asRecord(raw.runtimeApiAuth, `${path}.runtimeApiAuth`);
|
||||
const temporal = asRecord(raw.temporal, `${path}.temporal`);
|
||||
const activity = asRecord(temporal.activity, `${path}.temporal.activity`);
|
||||
const api = asRecord(raw.api, `${path}.api`);
|
||||
const worker = asRecord(raw.worker, `${path}.worker`);
|
||||
const web = asRecord(raw.web, `${path}.web`);
|
||||
return {
|
||||
publicExposure: nativePublicExposureConfig(raw.publicExposure, `${path}.publicExposure`),
|
||||
stateDir: relativeWorkspacePathField(stringField(raw, "stateDir", path), `${path}.stateDir`),
|
||||
runtimeApiUrl: httpUrlField(raw, "runtimeApiUrl", path),
|
||||
runtimeApiAuth: {
|
||||
sourceRef: stringField(runtimeApiAuth, "sourceRef", `${path}.runtimeApiAuth`),
|
||||
sourceKey: stringField(runtimeApiAuth, "sourceKey", `${path}.runtimeApiAuth`),
|
||||
},
|
||||
temporal: {
|
||||
address: stringField(temporal, "address", `${path}.temporal`),
|
||||
namespace: stringField(temporal, "namespace", `${path}.temporal`),
|
||||
taskQueue: stringField(temporal, "taskQueue", `${path}.temporal`),
|
||||
activity: {
|
||||
startToCloseTimeoutMs: numberField(activity, "startToCloseTimeoutMs", `${path}.temporal.activity`),
|
||||
retryInitialIntervalMs: numberField(activity, "retryInitialIntervalMs", `${path}.temporal.activity`),
|
||||
retryMaximumIntervalMs: numberField(activity, "retryMaximumIntervalMs", `${path}.temporal.activity`),
|
||||
retryMaximumAttempts: numberField(activity, "retryMaximumAttempts", `${path}.temporal.activity`),
|
||||
},
|
||||
},
|
||||
api: { bindHost: stringField(api, "bindHost", `${path}.api`), probeHost: stringField(api, "probeHost", `${path}.api`), port: numberField(api, "port", `${path}.api`) },
|
||||
worker: { bindHost: stringField(worker, "bindHost", `${path}.worker`), probeHost: stringField(worker, "probeHost", `${path}.worker`), healthPort: numberField(worker, "healthPort", `${path}.worker`) },
|
||||
web: { bindHost: stringField(web, "bindHost", `${path}.web`), probeHost: stringField(web, "probeHost", `${path}.web`), port: numberField(web, "port", `${path}.web`) },
|
||||
};
|
||||
}
|
||||
|
||||
function nativeTaskTreeConfig(value: unknown, path: string): NonNullable<HwlabRuntimeNativeDevelopmentSpec["tasktree"]> {
|
||||
const raw = asRecord(value, path);
|
||||
const api = asRecord(raw.api, `${path}.api`);
|
||||
|
||||
Reference in New Issue
Block a user