fix: preinstall 71freq hwpod runtime config
This commit is contained in:
@@ -71,6 +71,7 @@
|
||||
"hwlab-cloud-api",
|
||||
"hwlab-workbench-runtime",
|
||||
"hwlab-user-billing",
|
||||
"hwlab-project-management",
|
||||
"hwlab-cloud-web",
|
||||
"hwlab-gateway",
|
||||
"hwlab-edge-proxy",
|
||||
@@ -359,6 +360,10 @@
|
||||
"services": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/laneService" }
|
||||
},
|
||||
"configMaps": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/laneConfigMap" }
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -689,10 +694,65 @@
|
||||
"env": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"configMapMounts": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/configMapMount" }
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"laneConfigMap": {
|
||||
"type": "object",
|
||||
"required": ["name", "data"],
|
||||
"properties": {
|
||||
"name": { "$ref": "#/$defs/k8sName" },
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"additionalProperties": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"configMapMount": {
|
||||
"type": "object",
|
||||
"required": ["name", "configMapName", "mountPath"],
|
||||
"properties": {
|
||||
"name": { "$ref": "#/$defs/k8sName" },
|
||||
"configMapName": { "$ref": "#/$defs/k8sName" },
|
||||
"mountPath": { "type": "string", "pattern": "^/" },
|
||||
"readOnly": { "type": "boolean" },
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/configMapMountItem" }
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"configMapMountItem": {
|
||||
"type": "object",
|
||||
"required": ["key", "path"],
|
||||
"properties": {
|
||||
"key": { "type": "string", "minLength": 1 },
|
||||
"path": { "type": "string", "minLength": 1, "not": { "pattern": "(^/|(^|/)\\.\\.(/|$))" } }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"k8sName": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 63,
|
||||
"pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"
|
||||
},
|
||||
"repoPath": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
|
||||
@@ -91,6 +91,43 @@ lanes:
|
||||
node: G14
|
||||
sourceBranch: v0.2
|
||||
gitopsBranch: v0.2-gitops
|
||||
gitMirror:
|
||||
namespace: devops-infra
|
||||
serviceReadName: git-mirror-http
|
||||
serviceWriteName: git-mirror-write
|
||||
cachePvcName: hwlab-git-mirror-cache
|
||||
cachePvcStorage: 20Gi
|
||||
cacheHostPath: /var/lib/rancher/k3s/storage/hwlab-d601-v02-git-mirror-cache
|
||||
servicePort: 8080
|
||||
syncConfigMapName: git-mirror-sync-script
|
||||
egressProxy:
|
||||
mode: node-global
|
||||
required: true
|
||||
namespace: platform-infra
|
||||
serviceName: sub2api-egress-proxy
|
||||
port: 10808
|
||||
clientName: d601-global-proxy
|
||||
noProxy:
|
||||
- localhost
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
- 127.0.0.1:5000
|
||||
- localhost:5000
|
||||
- .svc
|
||||
- .svc.cluster.local
|
||||
- .cluster.local
|
||||
- kubernetes
|
||||
- kubernetes.default
|
||||
- kubernetes.default.svc
|
||||
- 10.0.0.0/8
|
||||
- 10.42.0.0/16
|
||||
- 10.43.0.0/16
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
- 82.156.23.220
|
||||
- 74.48.78.17
|
||||
- hyueapi.com
|
||||
- .hyueapi.com
|
||||
namespace: hwlab-v02
|
||||
endpoint: https://hwlab.74-48-78-17.nip.io
|
||||
publicEndpoints:
|
||||
@@ -575,6 +612,82 @@ lanes:
|
||||
bootConfig:
|
||||
template: default
|
||||
overrides: {}
|
||||
configMaps:
|
||||
- name: hwlab-v03-hwpod-preinstalled-specs
|
||||
labels:
|
||||
hwlab.pikastech.local/config-kind: hwpod-preinstalled-specs
|
||||
annotations:
|
||||
hwlab.pikastech.local/source-ref: config/hwlab-hwpod-preinstalls/constart-71freq-c.yaml#hwpodPreinstall
|
||||
hwlab.pikastech.local/verification-issue: pikasTech/HWLAB#2183
|
||||
data:
|
||||
constart-71freq-c.yaml: |-
|
||||
apiVersion: hwlab.dev/v0alpha1
|
||||
kind: Hwpod
|
||||
metadata:
|
||||
uid: CONSTART-71FREQ-C
|
||||
name: constart-71freq-c
|
||||
spec:
|
||||
targetDevice:
|
||||
board: ConStart 71-FREQ Controller
|
||||
mcu: STM32H723ZGTx
|
||||
flashBase: "0x08000000"
|
||||
workspace:
|
||||
path: "F:\\Work\\ConStart"
|
||||
toolchain: keil-mdk
|
||||
projectRoot: projects/71-00075-11
|
||||
keilProject: projects/71-00075-11/FirmWare/MDK-ARM/FREQ_Controller_FW.uvprojx
|
||||
keilTarget: FREQ_Controller_FW
|
||||
hexPath: projects/71-00075-11/FirmWare/MDK-ARM/FREQ_Controller_FW/FREQ_Controller_FW.hex
|
||||
mapPath: projects/71-00075-11/FirmWare/MDK-ARM/FREQ_Controller_FW/FREQ_Controller_FW.map
|
||||
keilCliPath: "C:\\Users\\liang\\.agents\\skills\\keil\\keil-cli.py"
|
||||
debugProbe:
|
||||
type: cmsis-dap
|
||||
adapter: keil
|
||||
probeUid: 3FD750C63E342E24
|
||||
probeName: MicroLink CMSIS-DAP
|
||||
programBackend: keil-headless
|
||||
autoBindUvoptx: true
|
||||
ioProbe:
|
||||
uart:
|
||||
id: uart/1
|
||||
port: COM4
|
||||
baudrate: 921600
|
||||
nodeBinding:
|
||||
nodeId: node-d601-f103-v2
|
||||
nodeType: pc-host
|
||||
constart-71freq-c.meta.json: |-
|
||||
{
|
||||
"contractVersion": "hwpod-spec-registry-v1",
|
||||
"source": {
|
||||
"kind": "preinstalled-yaml-first-spec",
|
||||
"workspaceRoot": "F:\\Work\\ConStart",
|
||||
"projectRoot": "projects/71-00075-11",
|
||||
"verificationIssue": "pikasTech/HWLAB#2183",
|
||||
"verifiedAt": "2026-06-26"
|
||||
}
|
||||
}
|
||||
- name: hwlab-v03-project-management-bootstrap
|
||||
labels:
|
||||
hwlab.pikastech.local/config-kind: project-management-bootstrap-sources
|
||||
annotations:
|
||||
hwlab.pikastech.local/source-ref: config/hwlab-project-management/constart-71freq-mdtodo.yaml#projectManagement.sources[0]
|
||||
hwlab.pikastech.local/verification-issue: pikasTech/HWLAB#2183
|
||||
data:
|
||||
sources.json: |-
|
||||
[
|
||||
{
|
||||
"sourceId": "constart-71freq-mdtodo",
|
||||
"sourceKind": "hwpod-workspace",
|
||||
"displayName": "71-FREQ MDTODO",
|
||||
"projectId": "project_constart_71freq",
|
||||
"hwpodId": "constart-71freq-c",
|
||||
"nodeId": "node-d601-f103-v2",
|
||||
"workspaceRootRef": "F:\\Work\\ConStart",
|
||||
"mdtodoRootRef": "docs/MDTODO",
|
||||
"maxFiles": 300,
|
||||
"capabilities": ["probe", "list", "read", "write", "reindex"]
|
||||
}
|
||||
]
|
||||
services:
|
||||
- serviceId: hwlab-cloud-api
|
||||
replicas: 1
|
||||
@@ -596,6 +709,7 @@ lanes:
|
||||
HWLAB_USER_BILLING_URL: http://hwlab-user-billing.hwlab-v03.svc.cluster.local:6670
|
||||
HWLAB_WORKBENCH_RUNTIME_URL: http://hwlab-workbench-runtime.hwlab-v03.svc.cluster.local:6671
|
||||
HWLAB_PROJECT_MANAGEMENT_URL: http://hwlab-project-management.hwlab-v03.svc.cluster.local:6672
|
||||
HWLAB_HWPOD_SPEC_REGISTRY_DIRS: /etc/hwlab/hwpod-specs
|
||||
HWLAB_USER_BILLING_LOGIN_RETRY_ATTEMPTS: "2"
|
||||
HWLAB_USER_BILLING_LOGIN_RETRY_DELAY_MS: "250"
|
||||
HWLAB_USER_BILLING_CODE_AGENT_ENABLED: "1"
|
||||
@@ -619,6 +733,11 @@ lanes:
|
||||
HWLAB_WORKBENCH_EMPTY_SESSION_GC_BATCH_SIZE: "100"
|
||||
HWLAB_WORKBENCH_EMPTY_SESSION_GC_JITTER_MS: "15000"
|
||||
HWLAB_WORKBENCH_EMPTY_SESSION_GC_FAILURE_BACKOFF_MS: "120000"
|
||||
configMapMounts:
|
||||
- name: hwpod-preinstalled-specs
|
||||
configMapName: hwlab-v03-hwpod-preinstalled-specs
|
||||
mountPath: /etc/hwlab/hwpod-specs
|
||||
readOnly: true
|
||||
- serviceId: hwlab-workbench-runtime
|
||||
env:
|
||||
HWLAB_WORKBENCH_RUNTIME_DB_URL: secretRef:hwlab-cloud-api-v03-db/database-url
|
||||
@@ -653,8 +772,14 @@ lanes:
|
||||
HWLAB_PROJECT_MANAGEMENT_SOURCE_NAME: HWLAB v0.3 MDTODO
|
||||
HWLAB_PROJECT_MANAGEMENT_PROJECT_ID: project_hwlab_v03
|
||||
HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_URL: http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667/v1/hwpod-node-ops
|
||||
HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_PATH: /etc/hwlab/project-management/sources.json
|
||||
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces
|
||||
OTEL_SERVICE_NAME: hwlab-project-management
|
||||
configMapMounts:
|
||||
- name: project-management-bootstrap
|
||||
configMapName: hwlab-v03-project-management-bootstrap
|
||||
mountPath: /etc/hwlab/project-management
|
||||
readOnly: true
|
||||
services:
|
||||
- serviceId: hwlab-cloud-api
|
||||
namespace: hwlab-dev
|
||||
|
||||
@@ -188,6 +188,50 @@ test("project management app configures HWPOD source and writes MDTODO content",
|
||||
}
|
||||
});
|
||||
|
||||
test("project management app bootstraps HWPOD sources from a mounted JSON file", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "hwlab-project-management-bootstrap-"));
|
||||
const defaultRoot = join(root, "default");
|
||||
const hwpodRoot = join(root, "hwpod-workspace");
|
||||
const mdtodoDir = join(hwpodRoot, "docs", "MDTODO");
|
||||
const configPath = join(root, "sources.json");
|
||||
try {
|
||||
await mkdir(defaultRoot, { recursive: true });
|
||||
await mkdir(mdtodoDir, { recursive: true });
|
||||
await writeFile(join(defaultRoot, "MDTODO.md"), "# Default\n\n## R1 Default\n", "utf8");
|
||||
await writeFile(join(mdtodoDir, "bootstrap.md"), "# Bootstrap\n\n## R1 From mounted source\n", "utf8");
|
||||
await writeFile(configPath, JSON.stringify({
|
||||
sources: [{
|
||||
sourceId: "constart-71freq-mdtodo",
|
||||
sourceKind: "hwpod-workspace",
|
||||
displayName: "71-FREQ MDTODO",
|
||||
projectId: "project_constart_71freq",
|
||||
hwpodId: "constart-71freq-c",
|
||||
nodeId: "node-d601-f103-v2",
|
||||
workspaceRootRef: hwpodRoot,
|
||||
mdtodoRootRef: "docs/MDTODO"
|
||||
}]
|
||||
}), "utf8");
|
||||
const app = createProjectManagementApp({
|
||||
env: { HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_PATH: configPath },
|
||||
store: createProjectManagementStore({ kind: "memory" }),
|
||||
sourceRoot: defaultRoot,
|
||||
sourceId: "default-mdtodo",
|
||||
projectId: "project_test",
|
||||
hwpodNodeOpsHandler: (plan) => executeHwpodNodeOpsPlan(plan)
|
||||
});
|
||||
|
||||
const ready = await app.fetch(new Request("http://service/health/ready"));
|
||||
expect(ready.status).toBe(200);
|
||||
const sources = await json(app, "/v1/project-management/mdtodo/sources");
|
||||
expect(sources.sources.map((source) => source.sourceId).sort()).toEqual(["constart-71freq-mdtodo", "default-mdtodo"]);
|
||||
const files = await json(app, "/v1/project-management/mdtodo/files?sourceId=constart-71freq-mdtodo");
|
||||
expect(files.files[0].relativePath).toBe("bootstrap.md");
|
||||
await app.close();
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
async function json(app, path) {
|
||||
const response = await app.fetch(new Request(`http://service${path}`, { headers: { "x-hwlab-actor-id": "usr_test" } }));
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { mutateMdtodoDocument } from "./mdtodo.ts";
|
||||
@@ -37,10 +38,23 @@ export function createProjectManagementApp(options = {}) {
|
||||
initializing = (async () => {
|
||||
const startedAt = new Date().toISOString();
|
||||
await store.ensureSchema();
|
||||
await store.upsertSource(defaultSource);
|
||||
const documents = await sourceAdapter.discover(defaultSource);
|
||||
await store.replaceProjection(defaultSource, documents);
|
||||
lastProjection = { ...sourceProjection(defaultSource, documents), startedAt };
|
||||
const bootstrapSources = await projectManagementBootstrapSources({ env, projectId, displayName, sourceRoot, maxFiles });
|
||||
const sources = dedupeProjectSources([defaultSource, ...bootstrapSources]);
|
||||
const projections = [];
|
||||
for (const source of sources) {
|
||||
await store.upsertSource(source);
|
||||
try {
|
||||
const documents = await sourceAdapter.discover(source);
|
||||
await store.replaceProjection(source, documents);
|
||||
projections.push({ ...sourceProjection(source, documents), status: "ready" });
|
||||
} catch (error) {
|
||||
if (source.sourceId === defaultSource.sourceId) throw error;
|
||||
const blocker = { code: error?.code ?? "project_source_projection_failed", message: error?.message ?? String(error) };
|
||||
projections.push({ ...sourceProjection(source, []), status: "blocked", blocker });
|
||||
logger.warn?.(JSON.stringify({ event: "project-management-bootstrap-source-projection-failed", serviceId, sourceId: source.sourceId, sourceKind: source.sourceKind, ...blocker }));
|
||||
}
|
||||
}
|
||||
lastProjection = summarizeProjectManagementProjection(projections, startedAt);
|
||||
initialized = true;
|
||||
lastError = null;
|
||||
logger.info?.(JSON.stringify({ event: "project-management-projection-ready", serviceId, ...lastProjection }));
|
||||
@@ -165,6 +179,56 @@ export function createProjectManagementApp(options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
async function projectManagementBootstrapSources({ env, projectId, displayName, sourceRoot, maxFiles }) {
|
||||
const inputs = await readProjectManagementBootstrapSourceInputs(env);
|
||||
return inputs.map((input, index) => {
|
||||
const normalized = normalizeProjectSourceInput(input, { projectId, displayName, rootRef: sourceRoot, maxFiles });
|
||||
if (!normalized.ok) {
|
||||
const error = Object.assign(new Error(`bootstrap source[${index}] invalid: ${normalized.message}`), {
|
||||
code: normalized.code,
|
||||
statusCode: normalized.status
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
return normalized.source;
|
||||
});
|
||||
}
|
||||
|
||||
async function readProjectManagementBootstrapSourceInputs(env) {
|
||||
const pathValue = String(env.HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_PATH ?? "").trim();
|
||||
const inlineValue = String(env.HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_JSON ?? "").trim();
|
||||
if (!pathValue && !inlineValue) return [];
|
||||
const text = pathValue ? await readFile(pathValue, "utf8") : inlineValue;
|
||||
const parsed = JSON.parse(text);
|
||||
if (Array.isArray(parsed)) return parsed;
|
||||
if (Array.isArray(parsed?.sources)) return parsed.sources;
|
||||
const error = Object.assign(new Error("Project management bootstrap sources must be a JSON array or an object with a sources array"), {
|
||||
code: "invalid_project_management_bootstrap_sources",
|
||||
statusCode: 400
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
function dedupeProjectSources(sources) {
|
||||
const byId = new Map();
|
||||
for (const source of sources) byId.set(source.sourceId, source);
|
||||
return [...byId.values()];
|
||||
}
|
||||
|
||||
function summarizeProjectManagementProjection(projections, startedAt) {
|
||||
if (projections.length === 1) return { ...projections[0], startedAt };
|
||||
return {
|
||||
sourceId: projections[0]?.sourceId ?? "project-management",
|
||||
sourceCount: projections.length,
|
||||
sources: projections,
|
||||
documentCount: projections.reduce((sum, item) => sum + Number(item.documentCount ?? 0), 0),
|
||||
taskCount: projections.reduce((sum, item) => sum + Number(item.taskCount ?? 0), 0),
|
||||
projectedAt: new Date().toISOString(),
|
||||
startedAt,
|
||||
valuesRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function healthPayload(mode, state) {
|
||||
const blockers = state.lastError ? [{ code: state.lastError.code ?? "project_management_not_ready", message: state.lastError.message ?? String(state.lastError) }] : [];
|
||||
return {
|
||||
|
||||
@@ -932,6 +932,33 @@ function applyDeployEnv(envList, deployService, namespace, profile = "dev") {
|
||||
}
|
||||
}
|
||||
|
||||
function applyDeployConfigMapMounts(podSpec, container, deployService) {
|
||||
const mounts = Array.isArray(deployService?.configMapMounts) ? deployService.configMapMounts : [];
|
||||
if (mounts.length === 0 || !podSpec || !container) return;
|
||||
podSpec.volumes ??= [];
|
||||
container.volumeMounts ??= [];
|
||||
for (const mount of mounts) {
|
||||
const name = String(mount?.name ?? "").trim();
|
||||
const configMapName = String(mount?.configMapName ?? "").trim();
|
||||
const mountPath = String(mount?.mountPath ?? "").trim();
|
||||
if (!name || !configMapName || !mountPath) continue;
|
||||
const items = Array.isArray(mount.items)
|
||||
? mount.items
|
||||
.map((item) => ({ key: String(item?.key ?? "").trim(), path: String(item?.path ?? "").trim() }))
|
||||
.filter((item) => item.key && item.path)
|
||||
: [];
|
||||
const volume = { name, configMap: { name: configMapName } };
|
||||
if (items.length > 0) volume.configMap.items = items;
|
||||
const existingVolume = podSpec.volumes.find((entry) => entry?.name === name);
|
||||
if (existingVolume) Object.assign(existingVolume, volume);
|
||||
else podSpec.volumes.push(volume);
|
||||
const volumeMount = { name, mountPath, readOnly: mount.readOnly !== false };
|
||||
const existingMount = container.volumeMounts.find((entry) => entry?.name === name);
|
||||
if (existingMount) Object.assign(existingMount, volumeMount);
|
||||
else container.volumeMounts.push(volumeMount);
|
||||
}
|
||||
}
|
||||
|
||||
function removeV02LegacySimulatorEnv(envList) {
|
||||
if (!Array.isArray(envList)) return envList;
|
||||
return envList.filter((entry) => !v02RemovedServiceEnvNames.has(entry?.name));
|
||||
@@ -1327,6 +1354,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch =
|
||||
if (Object.hasOwn(entry, "value")) entry.value = namespaceScopedHost(entry.value, namespace);
|
||||
}
|
||||
applyDeployEnv(container.env, deployService, namespace, profile);
|
||||
applyDeployConfigMapMounts(podTemplate.spec, container, deployService);
|
||||
if (runtimeLane) container.env = removeV02LegacySimulatorEnv(container.env);
|
||||
upsertEnv(container.env, "HWLAB_ENVIRONMENT", profileLabel);
|
||||
upsertEnv(container.env, "HWLAB_COMMIT_ID", runtimeCommit);
|
||||
@@ -5368,9 +5396,37 @@ function workbenchRuntimeRedisManifest({ profile = "v03", config, source }) {
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeKustomization({ profile = "dev", includeDeviceAgent = profile === "dev", externalPostgres = false, workbenchRedis = false } = {}) {
|
||||
function runtimeConfigMapsForProfile(deploy, profile) {
|
||||
if (!isRuntimeLane(profile)) return [];
|
||||
const configMaps = deploy?.lanes?.[profile]?.configMaps;
|
||||
if (!Array.isArray(configMaps)) return [];
|
||||
return configMaps
|
||||
.map((configMap) => cloneJson(configMap))
|
||||
.filter((configMap) => typeof configMap?.name === "string" && configMap.name.trim() && configMap.data && typeof configMap.data === "object" && !Array.isArray(configMap.data));
|
||||
}
|
||||
|
||||
function runtimeConfigMapsManifest({ configMaps, namespace, labels, annotations }) {
|
||||
return {
|
||||
apiVersion: "v1",
|
||||
kind: "List",
|
||||
items: configMaps.map((configMap) => ({
|
||||
apiVersion: "v1",
|
||||
kind: "ConfigMap",
|
||||
metadata: {
|
||||
name: configMap.name,
|
||||
namespace,
|
||||
labels: { ...labels, ...(configMap.labels ?? {}) },
|
||||
annotations: { ...annotations, ...(configMap.annotations ?? {}) }
|
||||
},
|
||||
data: Object.fromEntries(Object.entries(configMap.data).map(([key, value]) => [key, String(value)]))
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
function runtimeKustomization({ profile = "dev", includeDeviceAgent = profile === "dev", externalPostgres = false, workbenchRedis = false, runtimeConfigMaps = false } = {}) {
|
||||
const resources = ["namespace.yaml", "services.yaml", "health-contract.yaml", "workloads.yaml", "deepseek-proxy.yaml"];
|
||||
if (!isRuntimeLane(profile)) resources.splice(1, 0, "code-agent-codex-config.yaml");
|
||||
if (runtimeConfigMaps) resources.splice(3, 0, "configmaps.yaml");
|
||||
if (isRuntimeLane(profile)) {
|
||||
if (externalPostgres) resources.push("external-postgres.yaml");
|
||||
else resources.push("postgres.yaml");
|
||||
@@ -5510,6 +5566,7 @@ async function plannedFiles(args) {
|
||||
const includeDeviceAgent = profile === "dev";
|
||||
const externalPostgres = externalPostgresConfigForLane(deploy, profile);
|
||||
const workbenchRedis = workbenchRuntimeRedisConfigForProfile(deploy, profile);
|
||||
const runtimeConfigMaps = runtimeConfigMapsForProfile(deploy, profile);
|
||||
const profileLabels = { ...baseLabels, "hwlab.pikastech.local/environment": runtimeLabelForProfile(profile), "hwlab.pikastech.local/profile": runtimeLabelForProfile(profile) };
|
||||
const runtimeNamespace = cloneJson(namespaceTemplate);
|
||||
runtimeNamespace.metadata.name = namespace;
|
||||
@@ -5517,11 +5574,12 @@ async function plannedFiles(args) {
|
||||
annotate(runtimeNamespace.metadata, annotations);
|
||||
const runtimePath = runtimePathForProfile(profile);
|
||||
const endpoints = profileEndpoint(args, profile);
|
||||
putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile, includeDeviceAgent, externalPostgres: Boolean(externalPostgres), workbenchRedis: Boolean(workbenchRedis) }));
|
||||
putJson(`${runtimePath}/kustomization.yaml`, runtimeKustomization({ profile, includeDeviceAgent, externalPostgres: Boolean(externalPostgres), workbenchRedis: Boolean(workbenchRedis), runtimeConfigMaps: runtimeConfigMaps.length > 0 }));
|
||||
putJson(`${runtimePath}/namespace.yaml`, runtimeNamespace);
|
||||
if (!isRuntimeLane(profile)) putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations));
|
||||
putJson(`${runtimePath}/services.yaml`, transformServices({ services, namespace, labels: profileLabels, annotations, profile, deploy }));
|
||||
putJson(`${runtimePath}/health-contract.yaml`, transformHealthContract(health, namespace, profileLabels, annotations, endpoints.runtimeEndpoint, endpoints.webEndpoint, profile));
|
||||
if (runtimeConfigMaps.length > 0) putJson(`${runtimePath}/configmaps.yaml`, runtimeConfigMapsManifest({ configMaps: runtimeConfigMaps, namespace, labels: profileLabels, annotations }));
|
||||
putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, sourceBranch: args.sourceBranch, gitReadUrl: args.gitReadUrl, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, useDeployImages: args.useDeployImages, metricsSidecarSha256 }));
|
||||
putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, sourceBranch: args.sourceBranch, sourceRepo: args.sourceRepo, gitReadUrl: args.gitReadUrl, deploy, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages, metricsSidecarSha256 }));
|
||||
if (isRuntimeLane(profile) && externalPostgres) putJson(`${runtimePath}/external-postgres.yaml`, externalPostgresManifest({ profile, config: externalPostgres, source }));
|
||||
|
||||
@@ -532,6 +532,7 @@ test("v03 render keeps node identity as data instead of generated structure", as
|
||||
assert.ok(generatedPaths.includes("argocd/application-v03.yaml"));
|
||||
assert.ok(generatedPaths.includes("tekton-v03/pipeline.yaml"));
|
||||
assert.ok(generatedPaths.includes("runtime-v03/external-postgres.yaml"));
|
||||
assert.ok(generatedPaths.includes("runtime-v03/configmaps.yaml"));
|
||||
const pipeline = await readFile(path.join(outDir, "tekton-v03", "pipeline.yaml"), "utf8");
|
||||
assert.match(pipeline, /--external-service-report-dir \/workspace\/source\/service-results/u);
|
||||
assert.match(pipeline, /--ci-plan-path \/workspace\/source\/affected-services\.json/u);
|
||||
@@ -542,6 +543,19 @@ test("v03 render keeps node identity as data instead of generated structure", as
|
||||
|
||||
const workloads = await readFile(path.join(outDir, "runtime-v03", "workloads.yaml"), "utf8");
|
||||
const workloadsJson = JSON.parse(workloads);
|
||||
const configMapsJson = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "configmaps.yaml"), "utf8"));
|
||||
const configMapNames = (configMapsJson.items ?? []).map((item) => item.metadata?.name).sort();
|
||||
assert.deepEqual(configMapNames, ["hwlab-v03-hwpod-preinstalled-specs", "hwlab-v03-project-management-bootstrap"]);
|
||||
const cloudApi = (workloadsJson.items ?? []).find((item) => item.kind === "Deployment" && item.metadata?.name === "hwlab-cloud-api");
|
||||
const cloudApiContainer = collectContainersFromItem(cloudApi).find((container) => container.name === "hwlab-cloud-api");
|
||||
assert.ok((cloudApi?.spec?.template?.spec?.volumes ?? []).some((volume) => volume.name === "hwpod-preinstalled-specs" && volume.configMap?.name === "hwlab-v03-hwpod-preinstalled-specs"));
|
||||
assert.ok((cloudApiContainer?.volumeMounts ?? []).some((mount) => mount.name === "hwpod-preinstalled-specs" && mount.mountPath === "/etc/hwlab/hwpod-specs"));
|
||||
assert.ok((cloudApiContainer?.env ?? []).some((entry) => entry.name === "HWLAB_HWPOD_SPEC_REGISTRY_DIRS" && entry.value === "/etc/hwlab/hwpod-specs"));
|
||||
const projectManagement = (workloadsJson.items ?? []).find((item) => item.kind === "Deployment" && item.metadata?.name === "hwlab-project-management");
|
||||
const projectManagementContainer = collectContainersFromItem(projectManagement).find((container) => container.name === "hwlab-project-management");
|
||||
assert.ok((projectManagement?.spec?.template?.spec?.volumes ?? []).some((volume) => volume.name === "project-management-bootstrap" && volume.configMap?.name === "hwlab-v03-project-management-bootstrap"));
|
||||
assert.ok((projectManagementContainer?.volumeMounts ?? []).some((mount) => mount.name === "project-management-bootstrap" && mount.mountPath === "/etc/hwlab/project-management"));
|
||||
assert.ok((projectManagementContainer?.env ?? []).some((entry) => entry.name === "HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_PATH" && entry.value === "/etc/hwlab/project-management/sources.json"));
|
||||
const externalPostgres = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "external-postgres.yaml"), "utf8"));
|
||||
assert.deepEqual((externalPostgres.items ?? []).map((item) => item.kind), ["Service", "EndpointSlice"]);
|
||||
const externalPostgresSlice = (externalPostgres.items ?? []).find((item) => item.kind === "EndpointSlice");
|
||||
|
||||
Reference in New Issue
Block a user