feat(hwpod): 增加 L2 独立服务部署

This commit is contained in:
root
2026-07-21 04:24:39 +02:00
parent fd45e9938d
commit 5494c54121
11 changed files with 407 additions and 12 deletions
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bun
import path from "node:path";
const host = process.env.HWPOD_WEB_HOST || "0.0.0.0";
const port = positivePort(process.env.HWPOD_WEB_PORT, 8080);
const apiUrl = requiredUrl(process.env.HWPOD_API_URL, "HWPOD_API_URL");
const root = path.resolve(process.env.HWPOD_WEB_ROOT || "web/hwlab-cloud-web/hwpod/dist");
const server = Bun.serve({
hostname: host,
port,
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/health/live") return Response.json({ ok: true, serviceId: "hwlab-hwpod-web", status: "live", valuesPrinted: false });
if (url.pathname === "/health/ready" || url.pathname.startsWith("/v1/")) return proxyRequest(request, apiUrl, url);
const relativePath = url.pathname === "/" ? "index.html" : decodeURIComponent(url.pathname.replace(/^\/+/, ""));
const filePath = path.resolve(root, relativePath);
if (!filePath.startsWith(`${root}${path.sep}`) && filePath !== path.join(root, "index.html")) return new Response("not found", { status: 404 });
const file = Bun.file(filePath);
if (await file.exists()) return new Response(file, { headers: { "cache-control": relativePath === "index.html" ? "no-store" : "public, max-age=3600" } });
return new Response("not found", { status: 404 });
},
});
process.stdout.write(`${JSON.stringify({ serviceId: "hwlab-hwpod-web", status: "listening", host, port: server.port, apiAuthority: apiUrl, valuesPrinted: false })}\n`);
for (const signal of ["SIGINT", "SIGTERM"] as const) process.once(signal, () => { server.stop(true); process.exit(0); });
async function proxyRequest(request: Request, baseUrl: string, sourceUrl: URL) {
const target = new URL(`${sourceUrl.pathname}${sourceUrl.search}`, baseUrl);
const headers = new Headers(request.headers);
headers.delete("host");
return fetch(target, { method: request.method, headers, body: request.body, redirect: "manual", signal: request.signal });
}
function requiredUrl(value: string | undefined, name: string) { const parsed = new URL(String(value ?? "")); if (!/^https?:$/u.test(parsed.protocol)) throw new Error(`${name} must be an HTTP URL`); return parsed.toString().replace(/\/$/u, ""); }
function positivePort(value: string | undefined, fallback: number) { const parsed = Number.parseInt(String(value ?? fallback), 10); if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) throw new Error("HWPOD_WEB_PORT must be a valid port"); return parsed; }
+195 -1
View File
@@ -336,6 +336,12 @@ lanes:
port: 80
targetPort: 8080
nodePort: 32009
- name: hwlab-hwpod-web-public
selector:
app.kubernetes.io/name: hwlab-hwpod-web
port: 80
targetPort: 8080
nodePort: 32012
frp:
server:
address: 82.156.23.220
@@ -515,6 +521,9 @@ lanes:
- hwlab-harnessrl-worker
- hwlab-workbench-api
- hwlab-workbench-worker
- hwlab-hwpod-api
- hwlab-hwpod-worker
- hwlab-hwpod-web
- hwlab-cloud-web
- hwlab-gateway
- hwlab-edge-proxy
@@ -530,6 +539,9 @@ lanes:
hwlab-harnessrl-worker: deploy/runtime/boot/hwlab-harnessrl-worker.sh
hwlab-workbench-api: deploy/runtime/boot/hwlab-workbench-api.sh
hwlab-workbench-worker: deploy/runtime/boot/hwlab-workbench-worker.sh
hwlab-hwpod-api: deploy/runtime/boot/hwlab-hwpod-api.sh
hwlab-hwpod-worker: deploy/runtime/boot/hwlab-hwpod-worker.sh
hwlab-hwpod-web: deploy/runtime/boot/hwlab-hwpod-web.sh
hwlab-cloud-web: deploy/runtime/boot/hwlab-cloud-web.sh
hwlab-gateway: deploy/runtime/boot/hwlab-gateway.sh
hwlab-edge-proxy: deploy/runtime/boot/hwlab-edge-proxy.sh
@@ -743,6 +755,61 @@ lanes:
env:
TZ: Asia/Shanghai
observable: true
hwlab-hwpod-api:
runtimeKind: bun-command
entrypoint: cmd/hwlab-hwpod-api/main.ts
artifactKind: bun-command
healthPath: /health/ready
healthPort: 6681
componentPaths:
- cmd/hwlab-hwpod-api/
- internal/hwpod/
- internal/db/runtime-store-core.ts
- package.json
- package-lock.json
- deploy/runtime/boot/hwlab-hwpod-api.sh
env:
TZ: Asia/Shanghai
observable: true
hwlab-hwpod-worker:
runtimeKind: bun-command
entrypoint: cmd/hwlab-hwpod-worker/main.ts
artifactKind: bun-command
healthPath: /health/ready
healthPort: 6682
componentPaths:
- cmd/hwlab-hwpod-worker/
- internal/hwpod/
- internal/db/runtime-store-core.ts
- package.json
- package-lock.json
- deploy/runtime/boot/hwlab-hwpod-worker.sh
env:
TZ: Asia/Shanghai
observable: true
hwlab-hwpod-web:
runtimeKind: cloud-web
entrypoint: cmd/hwlab-hwpod-web/main.ts
artifactKind: cloud-web
healthPath: /health/live
healthPort: 8080
healthProbe:
startup:
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 60
componentPaths:
- cmd/hwlab-hwpod-web/
- web/hwlab-cloud-web/hwpod/
- web/hwlab-cloud-web/scripts/hwpod-native-vite.config.ts
- web/hwlab-cloud-web/package.json
- package.json
- package-lock.json
- deploy/runtime/boot/hwlab-hwpod-web.sh
env:
BUN_CONFIG_REGISTRY: https://registry.npmjs.org/
npm_config_registry: https://registry.npmjs.org/
observable: true
hwlab-cloud-web:
runtimeKind: cloud-web
entrypoint: web/hwlab-cloud-web/index.html
@@ -867,7 +934,7 @@ lanes:
bootConfig: &v03BootConfig
template: default
overrides: {}
configMaps:
configMaps: &v03ConfigMaps
- name: hwlab-v03-hwpod-preinstalled-specs
labels:
hwlab.pikastech.local/config-kind: hwpod-preinstalled-specs
@@ -902,11 +969,33 @@ lanes:
probeName: MicroLink CMSIS-DAP
programBackend: keil-headless
autoBindUvoptx: true
boardComm:
endpoints:
hwpod-node-ops:
kind: hwpod-node-ops
endpointRef: config/hwlab-gateway/constart-71freq.yaml#gateway.profile.nodeOps.publicUrl
freq71:
kind: jsonrpc-tcp
host: 192.168.0.154
port: 8000
role: frequency-controller
main41:
kind: jsonrpc-tcp
host: 192.168.0.151
port: 8000
role: main-controller
ioProbe:
uart:
id: uart/1
port: COM4
baudrate: 921600
probes:
main41.ai0.current:
endpointRef: main41
quantity: current
unit: mA
path: hardware/ai/0
valuePath: ai_current_mA
nodeBinding:
nodeId: node-d601-f103-v2
nodeType: pc-host
@@ -1172,6 +1261,58 @@ lanes:
HWLAB_KAFKA_EVENT_TOPIC: hwlab.event.v1
HWLAB_KAFKA_CLIENT_ID: hwlab-v03-workbench-worker
OTEL_SERVICE_NAME: hwlab-workbench-worker
- serviceId: hwlab-hwpod-api
replicas: 1
env:
HWPOD_API_HOST: 0.0.0.0
HWPOD_API_PORT: "6681"
HWPOD_TEMPORAL_ADDRESS: temporal-frontend.temporal.svc.cluster.local:7233
HWPOD_TEMPORAL_NAMESPACE: unidesk
HWPOD_TEMPORAL_TASK_QUEUE: hwlab-v03-hwpod
HWPOD_RUNTIME_API_URL: http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667
HWPOD_RUNTIME_API_AUTHORIZATION: secretRef:hwlab-v03-master-server-admin-api-key/api-key
HWPOD_RUNTIME_API_AUTHORIZATION_PREFIX: Bearer
HWPOD_SPEC_DATABASE_URL: secretRef:hwlab-cloud-api-v03-db/database-url
HWPOD_SPEC_DATABASE_SCHEMA: hwpod
HWPOD_SPEC_DATABASE_TABLE: runtime_specs
HWPOD_SPEC_DATABASE_CONNECTION_TIMEOUT_MS: "5000"
HWPOD_BUILTIN_SPEC_CONFIG_DIRECTORY: /etc/hwlab/hwpod-specs
OTEL_SERVICE_NAME: hwlab-hwpod-api
configMapMounts:
- name: hwpod-preinstalled-specs
configMapName: hwlab-v03-hwpod-preinstalled-specs
mountPath: /etc/hwlab/hwpod-specs
readOnly: true
- serviceId: hwlab-hwpod-worker
replicas: 1
env:
HWPOD_WORKER_HEALTH_HOST: 0.0.0.0
HWPOD_WORKER_HEALTH_PORT: "6682"
HWPOD_TEMPORAL_ADDRESS: temporal-frontend.temporal.svc.cluster.local:7233
HWPOD_TEMPORAL_NAMESPACE: unidesk
HWPOD_TEMPORAL_TASK_QUEUE: hwlab-v03-hwpod
HWPOD_RUNTIME_API_URL: http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667
HWPOD_RUNTIME_API_AUTHORIZATION: secretRef:hwlab-v03-master-server-admin-api-key/api-key
HWPOD_RUNTIME_API_AUTHORIZATION_PREFIX: Bearer
HWPOD_SPEC_DATABASE_URL: secretRef:hwlab-cloud-api-v03-db/database-url
HWPOD_SPEC_DATABASE_SCHEMA: hwpod
HWPOD_SPEC_DATABASE_TABLE: runtime_specs
HWPOD_SPEC_DATABASE_CONNECTION_TIMEOUT_MS: "5000"
HWPOD_BUILTIN_SPEC_CONFIG_DIRECTORY: /etc/hwlab/hwpod-specs
OTEL_SERVICE_NAME: hwlab-hwpod-worker
configMapMounts:
- name: hwpod-preinstalled-specs
configMapName: hwlab-v03-hwpod-preinstalled-specs
mountPath: /etc/hwlab/hwpod-specs
readOnly: true
- serviceId: hwlab-hwpod-web
replicas: 1
env:
HWPOD_WEB_HOST: 0.0.0.0
HWPOD_WEB_PORT: "8080"
HWPOD_API_URL: http://hwlab-hwpod-api.hwlab-v03.svc.cluster.local:6681
HWPOD_WEB_ROOT: web/hwlab-cloud-web/hwpod/dist
OTEL_SERVICE_NAME: hwlab-hwpod-web
production:
name: Production
node: NC01
@@ -1213,6 +1354,7 @@ lanes:
envReuseServices: *v03EnvReuseServices
bootScripts: *v03BootScripts
serviceDeclarations: *v03ServiceDeclarations
configMaps: *v03ConfigMaps
services:
- serviceId: hwlab-cloud-api
shutdownDrain:
@@ -1307,6 +1449,58 @@ lanes:
TASKTREE_TEMPORAL_TASK_QUEUE: hwlab-production-tasktree
TASKTREE_WORKER_HEALTH_PORT: "6674"
OTEL_SERVICE_NAME: hwlab-tasktree-worker
- serviceId: hwlab-hwpod-api
replicas: 1
env:
HWPOD_API_HOST: 0.0.0.0
HWPOD_API_PORT: "6681"
HWPOD_TEMPORAL_ADDRESS: temporal-frontend.temporal.svc.cluster.local:7233
HWPOD_TEMPORAL_NAMESPACE: unidesk
HWPOD_TEMPORAL_TASK_QUEUE: hwlab-production-hwpod
HWPOD_RUNTIME_API_URL: http://hwlab-cloud-api.hwlab-production.svc.cluster.local:6667
HWPOD_RUNTIME_API_AUTHORIZATION: secretRef:hwlab-production-master-server-admin-api-key/api-key
HWPOD_RUNTIME_API_AUTHORIZATION_PREFIX: Bearer
HWPOD_SPEC_DATABASE_URL: secretRef:hwlab-cloud-api-production-db/database-url
HWPOD_SPEC_DATABASE_SCHEMA: hwpod
HWPOD_SPEC_DATABASE_TABLE: runtime_specs
HWPOD_SPEC_DATABASE_CONNECTION_TIMEOUT_MS: "5000"
HWPOD_BUILTIN_SPEC_CONFIG_DIRECTORY: /etc/hwlab/hwpod-specs
OTEL_SERVICE_NAME: hwlab-hwpod-api
configMapMounts:
- name: hwpod-preinstalled-specs
configMapName: hwlab-v03-hwpod-preinstalled-specs
mountPath: /etc/hwlab/hwpod-specs
readOnly: true
- serviceId: hwlab-hwpod-worker
replicas: 1
env:
HWPOD_WORKER_HEALTH_HOST: 0.0.0.0
HWPOD_WORKER_HEALTH_PORT: "6682"
HWPOD_TEMPORAL_ADDRESS: temporal-frontend.temporal.svc.cluster.local:7233
HWPOD_TEMPORAL_NAMESPACE: unidesk
HWPOD_TEMPORAL_TASK_QUEUE: hwlab-production-hwpod
HWPOD_RUNTIME_API_URL: http://hwlab-cloud-api.hwlab-production.svc.cluster.local:6667
HWPOD_RUNTIME_API_AUTHORIZATION: secretRef:hwlab-production-master-server-admin-api-key/api-key
HWPOD_RUNTIME_API_AUTHORIZATION_PREFIX: Bearer
HWPOD_SPEC_DATABASE_URL: secretRef:hwlab-cloud-api-production-db/database-url
HWPOD_SPEC_DATABASE_SCHEMA: hwpod
HWPOD_SPEC_DATABASE_TABLE: runtime_specs
HWPOD_SPEC_DATABASE_CONNECTION_TIMEOUT_MS: "5000"
HWPOD_BUILTIN_SPEC_CONFIG_DIRECTORY: /etc/hwlab/hwpod-specs
OTEL_SERVICE_NAME: hwlab-hwpod-worker
configMapMounts:
- name: hwpod-preinstalled-specs
configMapName: hwlab-v03-hwpod-preinstalled-specs
mountPath: /etc/hwlab/hwpod-specs
readOnly: true
- serviceId: hwlab-hwpod-web
replicas: 1
env:
HWPOD_WEB_HOST: 0.0.0.0
HWPOD_WEB_PORT: "8080"
HWPOD_API_URL: http://hwlab-hwpod-api.hwlab-production.svc.cluster.local:6681
HWPOD_WEB_ROOT: web/hwlab-cloud-web/hwpod/dist
OTEL_SERVICE_NAME: hwlab-hwpod-web
envRecipe: *v03EnvRecipe
bootConfig: *v03BootConfig
services:
+42
View File
@@ -2,6 +2,48 @@
"apiVersion": "v1",
"kind": "List",
"items": [
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-hwpod-api",
"namespace": "hwlab-dev",
"labels": { "app.kubernetes.io/name": "hwlab-hwpod-api", "hwlab.pikastech.local/service-id": "hwlab-hwpod-api" }
},
"spec": {
"type": "ClusterIP",
"selector": { "app.kubernetes.io/name": "hwlab-hwpod-api" },
"ports": [{ "name": "http", "port": 6681, "targetPort": "http" }]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-hwpod-worker",
"namespace": "hwlab-dev",
"labels": { "app.kubernetes.io/name": "hwlab-hwpod-worker", "hwlab.pikastech.local/service-id": "hwlab-hwpod-worker" }
},
"spec": {
"type": "ClusterIP",
"selector": { "app.kubernetes.io/name": "hwlab-hwpod-worker" },
"ports": [{ "name": "health", "port": 6682, "targetPort": "health" }]
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "hwlab-hwpod-web",
"namespace": "hwlab-dev",
"labels": { "app.kubernetes.io/name": "hwlab-hwpod-web", "hwlab.pikastech.local/service-id": "hwlab-hwpod-web" }
},
"spec": {
"type": "ClusterIP",
"selector": { "app.kubernetes.io/name": "hwlab-hwpod-web" },
"ports": [{ "name": "http", "port": 8080, "targetPort": "http" }]
}
},
{
"apiVersion": "v1",
"kind": "Service",
+72
View File
@@ -2,6 +2,78 @@
"apiVersion": "v1",
"kind": "List",
"items": [
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "hwlab-hwpod-api",
"namespace": "hwlab-dev",
"labels": { "app.kubernetes.io/name": "hwlab-hwpod-api", "hwlab.pikastech.local/service-id": "hwlab-hwpod-api" }
},
"spec": {
"replicas": 0,
"selector": { "matchLabels": { "app.kubernetes.io/name": "hwlab-hwpod-api" } },
"template": {
"metadata": { "labels": { "app.kubernetes.io/name": "hwlab-hwpod-api", "hwlab.pikastech.local/service-id": "hwlab-hwpod-api" } },
"spec": { "containers": [{
"name": "hwlab-hwpod-api",
"image": "127.0.0.1:5000/hwlab/hwlab-hwpod-api:dev",
"ports": [{ "name": "http", "containerPort": 6681 }],
"env": [{ "name": "HWPOD_API_PORT", "value": "6681" }],
"readinessProbe": { "httpGet": { "path": "/health/ready", "port": "http" } },
"livenessProbe": { "httpGet": { "path": "/health/live", "port": "http" } }
}] }
}
}
},
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "hwlab-hwpod-worker",
"namespace": "hwlab-dev",
"labels": { "app.kubernetes.io/name": "hwlab-hwpod-worker", "hwlab.pikastech.local/service-id": "hwlab-hwpod-worker" }
},
"spec": {
"replicas": 0,
"selector": { "matchLabels": { "app.kubernetes.io/name": "hwlab-hwpod-worker" } },
"template": {
"metadata": { "labels": { "app.kubernetes.io/name": "hwlab-hwpod-worker", "hwlab.pikastech.local/service-id": "hwlab-hwpod-worker" } },
"spec": { "containers": [{
"name": "hwlab-hwpod-worker",
"image": "127.0.0.1:5000/hwlab/hwlab-hwpod-worker:dev",
"ports": [{ "name": "health", "containerPort": 6682 }],
"env": [{ "name": "HWPOD_WORKER_HEALTH_PORT", "value": "6682" }],
"readinessProbe": { "httpGet": { "path": "/health/ready", "port": "health" } },
"livenessProbe": { "httpGet": { "path": "/health/live", "port": "health" } }
}] }
}
}
},
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "hwlab-hwpod-web",
"namespace": "hwlab-dev",
"labels": { "app.kubernetes.io/name": "hwlab-hwpod-web", "hwlab.pikastech.local/service-id": "hwlab-hwpod-web" }
},
"spec": {
"replicas": 0,
"selector": { "matchLabels": { "app.kubernetes.io/name": "hwlab-hwpod-web" } },
"template": {
"metadata": { "labels": { "app.kubernetes.io/name": "hwlab-hwpod-web", "hwlab.pikastech.local/service-id": "hwlab-hwpod-web" } },
"spec": { "containers": [{
"name": "hwlab-hwpod-web",
"image": "127.0.0.1:5000/hwlab/hwlab-hwpod-web:dev",
"ports": [{ "name": "http", "containerPort": 8080 }],
"env": [{ "name": "HWPOD_WEB_PORT", "value": "8080" }],
"readinessProbe": { "httpGet": { "path": "/health/live", "port": "http" } },
"livenessProbe": { "httpGet": { "path": "/health/live", "port": "http" } }
}] }
}
}
},
{
"apiVersion": "apps/v1",
"kind": "Deployment",
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
set -eu
export HWLAB_SERVICE_ID=hwlab-hwpod-api
export HWLAB_SERVICE_ENTRYPOINT=cmd/hwlab-hwpod-api/main.ts
export HWPOD_API_PORT="${HWPOD_API_PORT:?HWPOD_API_PORT is required}"
export PORT="$HWPOD_API_PORT"
exec "${HWLAB_BUN_COMMAND:-bun}" run cmd/hwlab-hwpod-api/main.ts
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
set -eu
export HWLAB_SERVICE_ID=hwlab-hwpod-web
export HWLAB_SERVICE_ENTRYPOINT=cmd/hwlab-hwpod-web/main.ts
export HWPOD_WEB_PORT="${HWPOD_WEB_PORT:?HWPOD_WEB_PORT is required}"
export PORT="$HWPOD_WEB_PORT"
"${HWLAB_BUN_COMMAND:-bun}" run --cwd web/hwlab-cloud-web build:hwpod
exec "${HWLAB_BUN_COMMAND:-bun}" run cmd/hwlab-hwpod-web/main.ts
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
set -eu
export HWLAB_SERVICE_ID=hwlab-hwpod-worker
export HWLAB_SERVICE_ENTRYPOINT=cmd/hwlab-hwpod-worker/main.ts
export HWPOD_WORKER_HEALTH_PORT="${HWPOD_WORKER_HEALTH_PORT:?HWPOD_WORKER_HEALTH_PORT is required}"
export PORT="$HWPOD_WORKER_HEALTH_PORT"
exec "${HWLAB_BUN_COMMAND:-bun}" run cmd/hwlab-hwpod-worker/main.ts
+3 -1
View File
@@ -13,7 +13,9 @@ export function hwpodRuntime(env: Record<string, string | undefined> = process.e
const namespace = String(env.HWPOD_TEMPORAL_NAMESPACE ?? "unidesk").trim();
const taskQueue = String(env.HWPOD_TEMPORAL_TASK_QUEUE ?? "hwlab-v03-hwpod").trim();
const runtimeApiUrl = String(env.HWPOD_RUNTIME_API_URL ?? "").trim().replace(/\/+$/u, "");
const runtimeApiAuthorization = String(env.HWPOD_RUNTIME_API_AUTHORIZATION ?? "").trim();
const runtimeApiAuthorizationValue = String(env.HWPOD_RUNTIME_API_AUTHORIZATION ?? "").trim();
const runtimeApiAuthorizationPrefix = String(env.HWPOD_RUNTIME_API_AUTHORIZATION_PREFIX ?? "").trim();
const runtimeApiAuthorization = runtimeApiAuthorizationPrefix ? `${runtimeApiAuthorizationPrefix} ${runtimeApiAuthorizationValue}` : runtimeApiAuthorizationValue;
if (!runtimeApiUrl) throw codedError("hwpod_runtime_api_url_required", "HWPOD_RUNTIME_API_URL is required");
if (!runtimeApiAuthorization) throw codedError("hwpod_runtime_api_authorization_required", "HWPOD_RUNTIME_API_AUTHORIZATION is required");
return {
+24 -2
View File
@@ -5,7 +5,10 @@
*/
import { createHash } from "node:crypto";
import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import pg from "pg";
import { parse as parseYaml } from "yaml";
import { buildPostgresPoolConfig, postgresSslModeFromConnectionString } from "../db/runtime-store-core.ts";
import { normalizeHwpodSpec } from "./spec-document.ts";
@@ -29,7 +32,7 @@ export function hwpodSpecRegistryFromEnv(env: Record<string, string | undefined>
const schema = postgresIdentifier(requiredEnv(env, "HWPOD_SPEC_DATABASE_SCHEMA"), "HWPOD_SPEC_DATABASE_SCHEMA");
const table = postgresIdentifier(requiredEnv(env, "HWPOD_SPEC_DATABASE_TABLE"), "HWPOD_SPEC_DATABASE_TABLE");
const connectionTimeoutMs = positiveIntegerEnv(env, "HWPOD_SPEC_DATABASE_CONNECTION_TIMEOUT_MS");
const builtIns = parseBuiltIns(env.HWPOD_BUILTIN_SPEC_DOCUMENTS_JSON);
const builtIns = parseBuiltIns(env.HWPOD_BUILTIN_SPEC_DOCUMENTS_JSON, env.HWPOD_BUILTIN_SPEC_CONFIG_DIRECTORY);
return createHwpodSpecRegistry({ store: createPostgresHwpodRuntimeSpecStore({ databaseUrl, schema, table, connectionTimeoutMs }), builtIns });
}
@@ -163,7 +166,9 @@ function runtimeRow(row: any): HwpodRuntimeSpecRecord {
return { authority: "runtime", hwpodId, createdAt: isoTimestamp(row.created_at), updatedAt: isoTimestamp(row.updated_at), document };
}
function parseBuiltIns(raw: string | undefined): BuiltInInput[] {
function parseBuiltIns(raw: string | undefined, configDirectory: string | undefined): BuiltInInput[] {
if (String(raw ?? "").trim() && String(configDirectory ?? "").trim()) throw codedError("invalid_hwpod_builtin_specs", "HWPOD built-in specs must use either JSON or a config directory, not both");
if (String(configDirectory ?? "").trim()) return parseBuiltInConfigDirectory(String(configDirectory));
if (!String(raw ?? "").trim()) return [];
let value: unknown;
try { value = JSON.parse(String(raw)); }
@@ -177,6 +182,23 @@ function parseBuiltIns(raw: string | undefined): BuiltInInput[] {
});
}
function parseBuiltInConfigDirectory(directoryInput: string): BuiltInInput[] {
const directory = path.resolve(directoryInput.trim());
return readdirSync(directory, { withFileTypes: true })
.filter((entry) => entry.isFile() && /\.ya?ml$/iu.test(entry.name))
.map((entry) => entry.name)
.sort()
.map((fileName) => {
const source = readFileSync(path.join(directory, fileName), "utf8");
const document = parseYaml(source);
return {
configRef: `configMap:hwlab-v03-hwpod-preinstalled-specs/${fileName}`,
sha256: createHash("sha256").update(stableJson(document)).digest("hex"),
document,
};
});
}
function normalizeBuiltIns(inputs: BuiltInInput[]) {
const records = new Map<string, any>();
for (const input of inputs) {
+1
View File
@@ -24,6 +24,7 @@
"check:tsc-strict": "bun run deps --quiet && bun run scripts/tsc-check.ts --strict",
"typecheck": "bun run deps --quiet && vue-tsc --noEmit",
"build": "bun run deps --quiet && bun run scripts/build.ts",
"build:hwpod": "bun run deps --quiet && vite build --config scripts/hwpod-native-vite.config.ts",
"test": "bun run deps --quiet && vitest run",
"e2e:workbench": "bun run deps --quiet && bun run build && bunx playwright test -c playwright.workbench.config.ts",
"e2e:workbench:report": "bun run deps --quiet && bunx playwright show-report .state/workbench-e2e/html-report"
@@ -6,19 +6,18 @@
import path from "node:path";
import { defineConfig, type Plugin } from "vite";
const apiUrl = process.env.HWPOD_NATIVE_API_URL;
if (!apiUrl) throw new Error("HWPOD_NATIVE_API_URL is required");
export default defineConfig({
export default defineConfig(({ command }) => ({
root: path.resolve(import.meta.dirname, "../hwpod"),
server: {
build: { outDir: "dist", emptyOutDir: true },
server: command === "serve" ? {
host: process.env.HWPOD_WEB_HOST || "0.0.0.0",
port: requiredPort(process.env.HWPOD_WEB_PORT),
strictPort: true,
proxy: { "/v1": { target: apiUrl, changeOrigin: false }, "/health/ready": { target: apiUrl, changeOrigin: false } }
},
proxy: { "/v1": { target: requiredApiUrl(), changeOrigin: false }, "/health/ready": { target: requiredApiUrl(), changeOrigin: false } }
} : undefined,
plugins: [healthPlugin()]
});
}));
function healthPlugin(): Plugin { return { name: "hwpod-native-health", configureServer(server) { server.middlewares.use((request, response, next) => { if (request.url !== "/health/live") return next(); response.statusCode = 200; response.setHeader("content-type", "application/json"); response.end(JSON.stringify({ ok: true, serviceId: "hwlab-hwpod-web", status: "live", valuesPrinted: false })); }); } }; }
function requiredApiUrl() { const value = process.env.HWPOD_NATIVE_API_URL; if (!value) throw new Error("HWPOD_NATIVE_API_URL is required"); return value; }
function requiredPort(value: string | undefined) { const port = Number.parseInt(String(value ?? ""), 10); if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("HWPOD_WEB_PORT is required and must be valid"); return port; }