import { readFileSync } from "node:fs"; import { rootPath } from "./config"; export type HwlabRuntimeLane = "v02" | "v03"; export interface HwlabRuntimeNodeSpec { readonly id: string; readonly route: string; readonly kubeRoute: string; readonly sourceWorkspace: string; readonly gitopsRoot: string; readonly networkProfileId: string; readonly downloadProfileId: string; } export interface HwlabProxySpec { readonly http: string; readonly https: string; readonly all: string; readonly socks5?: string; readonly noProxy: readonly string[]; } export interface HwlabNetworkProfileSpec { readonly id: string; readonly proxy: HwlabProxySpec; readonly dockerBuildProxy: HwlabProxySpec; } export interface HwlabDownloadProfileSpec { readonly id: string; readonly git: { readonly proxyMode: "inherit" | "direct" | "none"; readonly retries: number; readonly timeoutSeconds: number; }; readonly npm: { readonly registry: string; readonly retries: number; readonly fetchTimeoutSeconds: number; }; readonly pip: { readonly indexUrl: string; readonly retries: number; readonly timeoutSeconds: number; }; readonly docker: { readonly registryMirrors: readonly string[]; readonly pullRetries: number; }; readonly curl: { readonly retries: number; readonly connectTimeoutSeconds: number; readonly maxTimeSeconds: number; }; } export interface HwlabRuntimeExternalPostgresComponentSpec { readonly secretName: string; readonly secretKey: string; readonly sourceRef: string; readonly envKey: string; readonly role: string; readonly authnKey?: string; readonly schema?: string; } export interface HwlabRuntimeExternalPostgresSpec { readonly provider: string; readonly configRef: string; readonly serviceName: string; readonly endpointAddress: string; readonly port: number; readonly sslmode: "require"; readonly database: string; readonly cloudApi: HwlabRuntimeExternalPostgresComponentSpec; readonly openfga: HwlabRuntimeExternalPostgresComponentSpec; } export interface HwlabRuntimeBuildkitSpec { readonly sidecarImage: string; } export interface HwlabRuntimeObservabilitySpec { readonly prometheusOperator: boolean; } export interface HwlabRuntimeImageRewriteSpec { readonly source: string; readonly target: string; } export interface HwlabRuntimeLaneSpec { readonly lane: HwlabRuntimeLane; readonly nodeId: string; readonly nodeRoute: string; readonly nodeKubeRoute: string; readonly gitopsRoot: string; readonly minor: number; readonly version: string; readonly sourceBranch: string; readonly workspace: string; readonly cicdRepo: string; readonly cicdRepoLock: string; readonly app: string; readonly pipeline: string; readonly pipelineRunPrefix: string; readonly serviceAccountName: string; readonly controlPlaneFieldManager: string; readonly gitUrl: string; readonly gitReadUrl: string; readonly gitWriteUrl: string; readonly argoRepoUrl: string; readonly gitopsBranch: string; readonly catalogPath: string; readonly runtimePath: string; readonly runtimeNamespace: string; readonly runtimeRenderDir: string; readonly tektonDir: string; readonly argoApplicationFile: string; readonly registryPrefix: string; readonly baseImage: string; readonly baseImageSource?: string; readonly serviceIds: readonly string[]; readonly publicWebUrl: string; readonly publicApiUrl: string; readonly stepEnv: Record; readonly buildkit?: HwlabRuntimeBuildkitSpec; readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec; readonly observability: HwlabRuntimeObservabilitySpec; readonly runtimeImageRewrites: readonly HwlabRuntimeImageRewriteSpec[]; readonly networkProfileId: string; readonly downloadProfileId: string; readonly networkProfile: HwlabNetworkProfileSpec; readonly downloadProfile: HwlabDownloadProfileSpec; } export const HWLAB_NODE_LANE_CONFIG_PATH = "config/hwlab-node-lanes.yaml"; interface HwlabLaneConfig { readonly id: HwlabRuntimeLane; readonly node: string; readonly minor: number; readonly version: string; readonly sourceBranch: string; readonly workspace: string; readonly cicdRepo: string; readonly cicdRepoLock: string; readonly app: string; readonly pipeline: string; readonly pipelineRunPrefix: string; readonly serviceAccountName: string; readonly controlPlaneFieldManager: string; readonly git: { readonly url: string; readonly readUrl: string; readonly writeUrl: string }; readonly argo: { readonly repoURL?: string }; readonly gitopsBranch: string; readonly catalogPath: string; readonly runtime: { readonly path: string; readonly namespace: string; readonly renderDir: string }; readonly tektonDir: string; readonly argoApplicationFile: string; readonly registryPrefix: string; readonly baseImage: string; readonly baseImageSource?: string; readonly serviceIds: readonly string[]; readonly public: { readonly webUrl: string; readonly apiUrl: string }; readonly stepEnv: Record; readonly buildkit?: HwlabRuntimeBuildkitSpec; readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec; readonly observability: HwlabRuntimeObservabilitySpec; readonly runtimeImageRewrites: readonly HwlabRuntimeImageRewriteSpec[]; } interface HwlabNodeLaneConfig { readonly requiredNoProxy: readonly string[]; readonly nodes: Record; readonly lanes: Record; readonly laneTargets: Partial>>; readonly networkProfiles: Record; readonly downloadProfiles: Record; } function asRecord(value: unknown, path: string): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`); return value as Record; } function stringField(obj: Record, key: string, path: string): string { const value = obj[key]; if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`); return value; } function numberField(obj: Record, key: string, path: string): number { const value = obj[key]; if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new Error(`${path}.${key} must be a positive integer`); return value; } function stringArrayField(obj: Record, key: string, path: string): string[] { const value = obj[key]; if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) { throw new Error(`${path}.${key} must be an array of non-empty strings`); } return [...value] as string[]; } function optionalStringField(obj: Record, key: string, path: string): string | undefined { const value = obj[key]; if (value === undefined) return undefined; if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`); return value; } function booleanField(obj: Record, key: string, path: string): boolean { const value = obj[key]; if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`); return value; } function sortedRecordEntries(value: unknown, path: string): Array<[string, Record]> { return Object.entries(asRecord(value, path)).map(([key, item]) => [key, asRecord(item, `${path}.${key}`)]); } function unique(values: readonly string[]): string[] { return [...new Set(values)]; } function mergeOptionalRecord(base: unknown, override: unknown): Record | undefined { if (base === undefined && override === undefined) return undefined; const baseRecord = base === undefined ? {} : asRecord(base, "base"); const overrideRecord = override === undefined ? {} : asRecord(override, "override"); return { ...baseRecord, ...overrideRecord }; } function optionalStringRecord(value: unknown, path: string): Record { if (value === undefined) return {}; const raw = asRecord(value, path); return Object.fromEntries(Object.entries(raw).map(([key, item]) => { if (typeof item !== "string" || item.length === 0) throw new Error(`${path}.${key} must be a non-empty string`); return [key, item]; })); } function proxyConfig(raw: Record, id: string, key: "proxy" | "dockerBuildProxy", requiredNoProxy: readonly string[]): HwlabProxySpec { const proxy = asRecord(raw[key], `networkProfiles.${id}.${key}`); return { http: stringField(proxy, "http", `networkProfiles.${id}.${key}`), https: stringField(proxy, "https", `networkProfiles.${id}.${key}`), all: stringField(proxy, "all", `networkProfiles.${id}.${key}`), socks5: optionalStringField(proxy, "socks5", `networkProfiles.${id}.${key}`), noProxy: unique([...stringArrayField(proxy, "noProxy", `networkProfiles.${id}.${key}`), ...requiredNoProxy]), }; } function networkProfileConfig(id: string, raw: Record, requiredNoProxy: readonly string[]): HwlabNetworkProfileSpec { return { id, proxy: proxyConfig(raw, id, "proxy", requiredNoProxy), dockerBuildProxy: proxyConfig(raw, id, "dockerBuildProxy", requiredNoProxy), }; } function downloadProfileConfig(id: string, raw: Record): HwlabDownloadProfileSpec { const git = asRecord(raw.git, `downloadProfiles.${id}.git`); const npm = asRecord(raw.npm, `downloadProfiles.${id}.npm`); const pip = asRecord(raw.pip, `downloadProfiles.${id}.pip`); const docker = asRecord(raw.docker, `downloadProfiles.${id}.docker`); const curl = asRecord(raw.curl, `downloadProfiles.${id}.curl`); const proxyMode = stringField(git, "proxyMode", `downloadProfiles.${id}.git`); if (proxyMode !== "inherit" && proxyMode !== "direct" && proxyMode !== "none") { throw new Error(`downloadProfiles.${id}.git.proxyMode must be inherit, direct, or none`); } return { id, git: { proxyMode, retries: numberField(git, "retries", `downloadProfiles.${id}.git`), timeoutSeconds: numberField(git, "timeoutSeconds", `downloadProfiles.${id}.git`), }, npm: { registry: stringField(npm, "registry", `downloadProfiles.${id}.npm`), retries: numberField(npm, "retries", `downloadProfiles.${id}.npm`), fetchTimeoutSeconds: numberField(npm, "fetchTimeoutSeconds", `downloadProfiles.${id}.npm`), }, pip: { indexUrl: stringField(pip, "indexUrl", `downloadProfiles.${id}.pip`), retries: numberField(pip, "retries", `downloadProfiles.${id}.pip`), timeoutSeconds: numberField(pip, "timeoutSeconds", `downloadProfiles.${id}.pip`), }, docker: { registryMirrors: stringArrayField(docker, "registryMirrors", `downloadProfiles.${id}.docker`), pullRetries: numberField(docker, "pullRetries", `downloadProfiles.${id}.docker`), }, curl: { retries: numberField(curl, "retries", `downloadProfiles.${id}.curl`), connectTimeoutSeconds: numberField(curl, "connectTimeoutSeconds", `downloadProfiles.${id}.curl`), maxTimeSeconds: numberField(curl, "maxTimeSeconds", `downloadProfiles.${id}.curl`), }, }; } function nodeConfig(id: string, raw: Record): HwlabRuntimeNodeSpec { return { id, route: stringField(raw, "route", `nodes.${id}`), kubeRoute: stringField(raw, "kubeRoute", `nodes.${id}`), sourceWorkspace: stringField(raw, "sourceWorkspace", `nodes.${id}`), gitopsRoot: stringField(raw, "gitopsRoot", `nodes.${id}`), networkProfileId: stringField(raw, "networkProfile", `nodes.${id}`), downloadProfileId: stringField(raw, "downloadProfile", `nodes.${id}`), }; } function isSupportedLaneId(id: string): id is HwlabRuntimeLane { return id === "v02" || id === "v03"; } function laneConfig(id: HwlabRuntimeLane, raw: Record): HwlabLaneConfig { const git = asRecord(raw.git, `lanes.${id}.git`); const argo = raw.argo === undefined ? {} : asRecord(raw.argo, `lanes.${id}.argo`); const runtime = asRecord(raw.runtime, `lanes.${id}.runtime`); const publicUrls = asRecord(raw.public, `lanes.${id}.public`); const minor = numberField(raw, "minor", `lanes.${id}`); const version = stringField(raw, "version", `lanes.${id}`); if (version !== `v0.${minor}`) throw new Error(`lanes.${id}.version must equal v0.${minor}`); return { id, node: stringField(raw, "node", `lanes.${id}`), minor, version, sourceBranch: stringField(raw, "sourceBranch", `lanes.${id}`), workspace: stringField(raw, "workspace", `lanes.${id}`), cicdRepo: stringField(raw, "cicdRepo", `lanes.${id}`), cicdRepoLock: stringField(raw, "cicdRepoLock", `lanes.${id}`), app: stringField(raw, "app", `lanes.${id}`), pipeline: stringField(raw, "pipeline", `lanes.${id}`), pipelineRunPrefix: stringField(raw, "pipelineRunPrefix", `lanes.${id}`), serviceAccountName: stringField(raw, "serviceAccountName", `lanes.${id}`), controlPlaneFieldManager: stringField(raw, "controlPlaneFieldManager", `lanes.${id}`), git: { url: stringField(git, "url", `lanes.${id}.git`), readUrl: stringField(git, "readUrl", `lanes.${id}.git`), writeUrl: stringField(git, "writeUrl", `lanes.${id}.git`), }, argo: { repoURL: optionalStringField(argo, "repoURL", `lanes.${id}.argo`), }, gitopsBranch: stringField(raw, "gitopsBranch", `lanes.${id}`), catalogPath: stringField(raw, "catalogPath", `lanes.${id}`), runtime: { path: stringField(runtime, "path", `lanes.${id}.runtime`), namespace: stringField(runtime, "namespace", `lanes.${id}.runtime`), renderDir: stringField(runtime, "renderDir", `lanes.${id}.runtime`), }, tektonDir: stringField(raw, "tektonDir", `lanes.${id}`), argoApplicationFile: stringField(raw, "argoApplicationFile", `lanes.${id}`), registryPrefix: stringField(raw, "registryPrefix", `lanes.${id}`), baseImage: stringField(raw, "baseImage", `lanes.${id}`), baseImageSource: optionalStringField(raw, "baseImageSource", `lanes.${id}`), serviceIds: stringArrayField(raw, "serviceIds", `lanes.${id}`), public: { webUrl: stringField(publicUrls, "webUrl", `lanes.${id}.public`), apiUrl: stringField(publicUrls, "apiUrl", `lanes.${id}.public`), }, stepEnv: optionalStringRecord(raw.stepEnv, `lanes.${id}.stepEnv`), buildkit: buildkitConfig(raw.buildkit, `lanes.${id}.buildkit`), externalPostgres: externalPostgresConfig(raw.externalPostgres, `lanes.${id}.externalPostgres`), observability: observabilityConfig(raw.observability, `lanes.${id}.observability`), runtimeImageRewrites: runtimeImageRewritesConfig(raw.runtimeImageRewrites, `lanes.${id}.runtimeImageRewrites`), }; } function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record, targetRaw: Record): HwlabLaneConfig { const merged: Record = { ...baseRaw, ...targetRaw, node: nodeId, git: mergeOptionalRecord(baseRaw.git, targetRaw.git), argo: mergeOptionalRecord(baseRaw.argo, targetRaw.argo), runtime: mergeOptionalRecord(baseRaw.runtime, targetRaw.runtime), public: mergeOptionalRecord(baseRaw.public, targetRaw.public), stepEnv: mergeOptionalRecord(baseRaw.stepEnv, targetRaw.stepEnv) ?? {}, buildkit: mergeOptionalRecord(baseRaw.buildkit, targetRaw.buildkit), externalPostgres: mergeOptionalRecord(baseRaw.externalPostgres, targetRaw.externalPostgres), observability: mergeOptionalRecord(baseRaw.observability, targetRaw.observability), }; delete merged.targets; return laneConfig(id, merged); } function buildkitConfig(value: unknown, path: string): HwlabRuntimeBuildkitSpec | undefined { if (value === undefined) return undefined; const raw = asRecord(value, path); return { sidecarImage: stringField(raw, "sidecarImage", path), }; } function externalPostgresComponentConfig(value: unknown, path: string): HwlabRuntimeExternalPostgresComponentSpec { const raw = asRecord(value, path); return { secretName: stringField(raw, "secretName", path), secretKey: stringField(raw, "secretKey", path), sourceRef: stringField(raw, "sourceRef", path), envKey: stringField(raw, "envKey", path), role: stringField(raw, "role", path), authnKey: optionalStringField(raw, "authnKey", path), schema: optionalStringField(raw, "schema", path), }; } function externalPostgresConfig(value: unknown, path: string): HwlabRuntimeExternalPostgresSpec | undefined { if (value === undefined) return undefined; const raw = asRecord(value, path); const sslmode = stringField(raw, "sslmode", path); if (sslmode !== "require") throw new Error(`${path}.sslmode must be require`); return { provider: stringField(raw, "provider", path), configRef: stringField(raw, "configRef", path), serviceName: stringField(raw, "serviceName", path), endpointAddress: stringField(raw, "endpointAddress", path), port: numberField(raw, "port", path), sslmode, database: stringField(raw, "database", path), cloudApi: externalPostgresComponentConfig(raw.cloudApi, `${path}.cloudApi`), openfga: externalPostgresComponentConfig(raw.openfga, `${path}.openfga`), }; } function observabilityConfig(value: unknown, path: string): HwlabRuntimeObservabilitySpec { const raw = asRecord(value, path); return { prometheusOperator: booleanField(raw, "prometheusOperator", path), }; } function runtimeImageRewritesConfig(value: unknown, path: string): HwlabRuntimeImageRewriteSpec[] { if (value === undefined) return []; if (!Array.isArray(value)) throw new Error(`${path} must be an array`); return value.map((item, index) => { const raw = asRecord(item, `${path}[${index}]`); return { source: stringField(raw, "source", `${path}[${index}]`), target: stringField(raw, "target", `${path}[${index}]`), }; }); } function readHwlabNodeLaneConfig(): HwlabNodeLaneConfig { const path = rootPath(HWLAB_NODE_LANE_CONFIG_PATH); const raw = readFileSync(path, "utf8"); const parsed = asRecord(Bun.YAML.parse(raw) as unknown, HWLAB_NODE_LANE_CONFIG_PATH); const requiredNoProxy = stringArrayField(parsed, "requiredNoProxy", HWLAB_NODE_LANE_CONFIG_PATH); for (const required of ["hyueapi.com", ".hyueapi.com"]) { if (!requiredNoProxy.includes(required)) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.requiredNoProxy must include ${required}`); } const nodes = Object.fromEntries(sortedRecordEntries(parsed.nodes, "nodes").map(([id, item]) => [id, nodeConfig(id, item)])); const networkProfiles = Object.fromEntries( sortedRecordEntries(parsed.networkProfiles, "networkProfiles").map(([id, item]) => [id, networkProfileConfig(id, item, requiredNoProxy)]), ); const downloadProfiles = Object.fromEntries( sortedRecordEntries(parsed.downloadProfiles, "downloadProfiles").map(([id, item]) => [id, downloadProfileConfig(id, item)]), ); const laneEntries = sortedRecordEntries(parsed.lanes, "lanes"); const lanes = Object.fromEntries(laneEntries.map(([id, item]) => { if (!isSupportedLaneId(id)) throw new Error(`lanes.${id} is not supported by this CLI build`); return [id, laneConfig(id, item)]; })) as Record; const laneTargets: Partial>> = {}; for (const [id, item] of laneEntries) { if (!isSupportedLaneId(id) || item.targets === undefined) continue; laneTargets[id] = Object.fromEntries(sortedRecordEntries(item.targets, `lanes.${id}.targets`).map(([nodeId, target]) => [ nodeId, laneTargetConfig(id, nodeId, item, target), ])); } for (const node of Object.values(nodes)) { if (networkProfiles[node.networkProfileId] === undefined) throw new Error(`nodes.${node.id}.networkProfile references missing profile ${node.networkProfileId}`); if (downloadProfiles[node.downloadProfileId] === undefined) throw new Error(`nodes.${node.id}.downloadProfile references missing profile ${node.downloadProfileId}`); } for (const lane of [...Object.values(lanes), ...Object.values(laneTargets).flatMap((targets) => Object.values(targets))]) { if (nodes[lane.node] === undefined) throw new Error(`lanes.${lane.id}.node references missing node ${lane.node}`); } return { requiredNoProxy, nodes, lanes, laneTargets, networkProfiles, downloadProfiles }; } const HWLAB_NODE_LANE_CONFIG = readHwlabNodeLaneConfig(); function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec { const node = HWLAB_NODE_LANE_CONFIG.nodes[config.node]; const networkProfile = HWLAB_NODE_LANE_CONFIG.networkProfiles[node.networkProfileId]; const downloadProfile = HWLAB_NODE_LANE_CONFIG.downloadProfiles[node.downloadProfileId]; return { lane: config.id, nodeId: node.id, nodeRoute: node.route, nodeKubeRoute: node.kubeRoute, gitopsRoot: node.gitopsRoot, minor: config.minor, version: config.version, sourceBranch: config.sourceBranch, workspace: config.workspace, cicdRepo: config.cicdRepo, cicdRepoLock: config.cicdRepoLock, app: config.app, pipeline: config.pipeline, pipelineRunPrefix: config.pipelineRunPrefix, serviceAccountName: config.serviceAccountName, controlPlaneFieldManager: config.controlPlaneFieldManager, gitUrl: config.git.url, gitReadUrl: config.git.readUrl, gitWriteUrl: config.git.writeUrl, argoRepoUrl: config.argo.repoURL ?? config.git.readUrl, gitopsBranch: config.gitopsBranch, catalogPath: config.catalogPath, runtimePath: config.runtime.path, runtimeNamespace: config.runtime.namespace, runtimeRenderDir: config.runtime.renderDir, tektonDir: config.tektonDir, argoApplicationFile: config.argoApplicationFile, registryPrefix: config.registryPrefix, baseImage: config.baseImage, ...(config.baseImageSource === undefined ? {} : { baseImageSource: config.baseImageSource }), serviceIds: config.serviceIds, publicWebUrl: config.public.webUrl, publicApiUrl: config.public.apiUrl, stepEnv: config.stepEnv, ...(config.buildkit === undefined ? {} : { buildkit: config.buildkit }), ...(config.externalPostgres === undefined ? {} : { externalPostgres: config.externalPostgres }), observability: config.observability, runtimeImageRewrites: config.runtimeImageRewrites, networkProfileId: networkProfile.id, downloadProfileId: downloadProfile.id, networkProfile, downloadProfile, }; } const RUNTIME_LANE_SPECS = Object.fromEntries( Object.values(HWLAB_NODE_LANE_CONFIG.lanes).map((config) => [config.id, buildRuntimeLaneSpec(config)]), ) as Record; export function isHwlabRuntimeLane(value: string): value is HwlabRuntimeLane { return Object.prototype.hasOwnProperty.call(RUNTIME_LANE_SPECS, value); } export function hwlabRuntimeLaneSpec(lane: HwlabRuntimeLane): HwlabRuntimeLaneSpec { return RUNTIME_LANE_SPECS[lane]; } export function hwlabRuntimeLaneSpecForNode(lane: HwlabRuntimeLane, nodeId: string): HwlabRuntimeLaneSpec { const targetSpec = HWLAB_NODE_LANE_CONFIG.laneTargets[lane]?.[nodeId]; if (targetSpec !== undefined) return buildRuntimeLaneSpec(targetSpec); const defaultSpec = RUNTIME_LANE_SPECS[lane]; if (defaultSpec.nodeId === nodeId) return defaultSpec; const knownNodes = unique([defaultSpec.nodeId, ...Object.keys(HWLAB_NODE_LANE_CONFIG.laneTargets[lane] ?? {})]).join(", "); throw new Error(`lane ${lane} has no target for node ${nodeId}; known nodes: ${knownNodes}`); } export function hwlabRuntimeLaneIds(): HwlabRuntimeLane[] { return Object.keys(RUNTIME_LANE_SPECS) as HwlabRuntimeLane[]; } export function hwlabRuntimeNodeIds(): string[] { return Object.keys(HWLAB_NODE_LANE_CONFIG.nodes); } export function hwlabRuntimeLaneConfigPath(): string { return HWLAB_NODE_LANE_CONFIG_PATH; } export function hwlabRequiredNoProxyEntries(): string[] { return [...HWLAB_NODE_LANE_CONFIG.requiredNoProxy]; }