feat: add HWLAB G14 lane profile config

This commit is contained in:
Codex
2026-06-08 10:32:10 +00:00
parent 6b48a9e682
commit b3476b49f1
6 changed files with 573 additions and 61 deletions
+332 -50
View File
@@ -1,12 +1,65 @@
import { readFileSync } from "node:fs";
import { rootPath } from "./config";
export type HwlabRuntimeLane = "v02" | "v03";
export interface HwlabRuntimeLaneConfig {
readonly lane: HwlabRuntimeLane;
readonly minor: number;
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 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 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;
@@ -33,62 +86,279 @@ export interface HwlabRuntimeLaneSpec {
readonly serviceIds: readonly string[];
readonly publicWebUrl: string;
readonly publicApiUrl: string;
readonly networkProfileId: string;
readonly downloadProfileId: string;
readonly networkProfile: HwlabNetworkProfileSpec;
readonly downloadProfile: HwlabDownloadProfileSpec;
}
export const HWLAB_RUNTIME_LANE_CONFIGS = [
{ lane: "v02", minor: 2 },
{ lane: "v03", minor: 3 },
] as const satisfies readonly HwlabRuntimeLaneConfig[];
export const HWLAB_G14_LANE_CONFIG_PATH = "config/hwlab-g14-lanes.yaml";
const HWLAB_GIT_URL = "git@github.com:pikasTech/HWLAB.git";
const HWLAB_GIT_READ_URL = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
const HWLAB_GIT_WRITE_URL = "http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
const HWLAB_REGISTRY_PREFIX = "127.0.0.1:5000/hwlab";
const HWLAB_BASE_IMAGE = "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim";
const HWLAB_SERVICE_IDS = [
"hwlab-cloud-api",
"hwlab-cloud-web",
"hwlab-gateway",
"hwlab-edge-proxy",
"hwlab-agent-skills",
] as const;
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 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 serviceIds: readonly string[];
readonly public: { readonly webUrl: string; readonly apiUrl: string };
}
function buildRuntimeLaneSpec(config: HwlabRuntimeLaneConfig): HwlabRuntimeLaneSpec {
const version = `v0.${config.minor}`;
const lane = config.lane;
interface HwlabG14LaneConfig {
readonly requiredNoProxy: readonly string[];
readonly nodes: Record<string, HwlabRuntimeNodeSpec>;
readonly lanes: Record<HwlabRuntimeLane, HwlabLaneConfig>;
readonly networkProfiles: Record<string, HwlabNetworkProfileSpec>;
readonly downloadProfiles: Record<string, HwlabDownloadProfileSpec>;
}
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
return value as Record<string, unknown>;
}
function stringField(obj: Record<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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 sortedRecordEntries(value: unknown, path: string): Array<[string, Record<string, unknown>]> {
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 proxyConfig(raw: Record<string, unknown>, id: string, key: "proxy" | "dockerBuildProxy", requiredNoProxy: readonly string[]): HwlabProxySpec {
const proxy = asRecord(raw[key], `networkProfiles.${id}.${key}`);
return {
lane,
minor: config.minor,
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<string, unknown>, requiredNoProxy: readonly string[]): HwlabNetworkProfileSpec {
return {
id,
proxy: proxyConfig(raw, id, "proxy", requiredNoProxy),
dockerBuildProxy: proxyConfig(raw, id, "dockerBuildProxy", requiredNoProxy),
};
}
function downloadProfileConfig(id: string, raw: Record<string, unknown>): 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`) },
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<string, unknown>): 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<string, unknown>): HwlabLaneConfig {
const git = asRecord(raw.git, `lanes.${id}.git`);
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: version,
workspace: `/root/hwlab-${lane}`,
cicdRepo: `/root/hwlab-${lane}-cicd.git`,
cicdRepoLock: `/tmp/hwlab-${lane}-cicd-repo.lock`,
app: `hwlab-g14-${lane}`,
pipeline: `hwlab-${lane}-ci-image-publish`,
pipelineRunPrefix: `hwlab-${lane}-ci-poll`,
serviceAccountName: `hwlab-${lane}-tekton-runner`,
controlPlaneFieldManager: `unidesk-hwlab-${lane}-control-plane`,
gitUrl: HWLAB_GIT_URL,
gitReadUrl: HWLAB_GIT_READ_URL,
gitWriteUrl: HWLAB_GIT_WRITE_URL,
gitopsBranch: `${version}-gitops`,
catalogPath: `deploy/artifact-catalog.${lane}.json`,
runtimePath: `deploy/gitops/g14/runtime-${lane}`,
runtimeNamespace: `hwlab-${lane}`,
runtimeRenderDir: `runtime-${lane}`,
tektonDir: `tekton-${lane}`,
argoApplicationFile: `application-${lane}.yaml`,
registryPrefix: HWLAB_REGISTRY_PREFIX,
baseImage: HWLAB_BASE_IMAGE,
serviceIds: HWLAB_SERVICE_IDS,
publicWebUrl: `http://74.48.78.17:${19466 + config.minor * 100}`,
publicApiUrl: `http://74.48.78.17:${19467 + config.minor * 100}`,
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`),
},
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}`),
serviceIds: stringArrayField(raw, "serviceIds", `lanes.${id}`),
public: {
webUrl: stringField(publicUrls, "webUrl", `lanes.${id}.public`),
apiUrl: stringField(publicUrls, "apiUrl", `lanes.${id}.public`),
},
};
}
function readHwlabG14LaneConfig(): HwlabG14LaneConfig {
const path = rootPath(HWLAB_G14_LANE_CONFIG_PATH);
const raw = readFileSync(path, "utf8");
const parsed = asRecord(Bun.YAML.parse(raw) as unknown, HWLAB_G14_LANE_CONFIG_PATH);
const requiredNoProxy = stringArrayField(parsed, "requiredNoProxy", HWLAB_G14_LANE_CONFIG_PATH);
for (const required of ["hyueapi.com", ".hyueapi.com"]) {
if (!requiredNoProxy.includes(required)) throw new Error(`${HWLAB_G14_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 lanes = Object.fromEntries(sortedRecordEntries(parsed.lanes, "lanes").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<HwlabRuntimeLane, HwlabLaneConfig>;
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)) {
if (nodes[lane.node] === undefined) throw new Error(`lanes.${lane.id}.node references missing node ${lane.node}`);
}
return { requiredNoProxy, nodes, lanes, networkProfiles, downloadProfiles };
}
const HWLAB_G14_LANE_CONFIG = readHwlabG14LaneConfig();
function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
const node = HWLAB_G14_LANE_CONFIG.nodes[config.node];
const networkProfile = HWLAB_G14_LANE_CONFIG.networkProfiles[node.networkProfileId];
const downloadProfile = HWLAB_G14_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,
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,
serviceIds: config.serviceIds,
publicWebUrl: config.public.webUrl,
publicApiUrl: config.public.apiUrl,
networkProfileId: networkProfile.id,
downloadProfileId: downloadProfile.id,
networkProfile,
downloadProfile,
};
}
const RUNTIME_LANE_SPECS = Object.fromEntries(
HWLAB_RUNTIME_LANE_CONFIGS.map((config) => [config.lane, buildRuntimeLaneSpec(config)]),
Object.values(HWLAB_G14_LANE_CONFIG.lanes).map((config) => [config.id, buildRuntimeLaneSpec(config)]),
) as Record<HwlabRuntimeLane, HwlabRuntimeLaneSpec>;
export function isHwlabRuntimeLane(value: string): value is HwlabRuntimeLane {
@@ -100,5 +370,17 @@ export function hwlabRuntimeLaneSpec(lane: HwlabRuntimeLane): HwlabRuntimeLaneSp
}
export function hwlabRuntimeLaneIds(): HwlabRuntimeLane[] {
return HWLAB_RUNTIME_LANE_CONFIGS.map((config) => config.lane);
return Object.keys(RUNTIME_LANE_SPECS) as HwlabRuntimeLane[];
}
export function hwlabRuntimeNodeIds(): string[] {
return Object.keys(HWLAB_G14_LANE_CONFIG.nodes);
}
export function hwlabRuntimeLaneConfigPath(): string {
return HWLAB_G14_LANE_CONFIG_PATH;
}
export function hwlabRequiredNoProxyEntries(): string[] {
return [...HWLAB_G14_LANE_CONFIG.requiredNoProxy];
}
+22 -2
View File
@@ -4,7 +4,7 @@ import { createHash, randomBytes } from "node:crypto";
import { repoRoot, rootPath, type Config } from "./config";
import { runCommand } from "./command";
import { readJob, startJob } from "./jobs";
import { hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "./hwlab-g14-lanes";
import { hwlabRequiredNoProxyEntries, hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "./hwlab-g14-lanes";
const HWLAB_REPO = "pikasTech/HWLAB";
const G14_SOURCE_BRANCH = "G14";
@@ -3208,7 +3208,7 @@ function deleteV02ObsoleteCronJobs(dryRun: boolean): CommandJsonResult {
], 60_000);
}
function runtimeLanePipelineRunManifest(spec: HwlabRuntimeLaneSpec, sourceCommit: string): Record<string, unknown> {
export function runtimeLanePipelineRunManifest(spec: HwlabRuntimeLaneSpec, sourceCommit: string): Record<string, unknown> {
const pipelineRun = runtimeLanePipelineRunName(spec, sourceCommit);
return {
apiVersion: "tekton.dev/v1",
@@ -3225,6 +3225,10 @@ function runtimeLanePipelineRunManifest(spec: HwlabRuntimeLaneSpec, sourceCommit
annotations: {
"hwlab.pikastech.local/source-branch": spec.sourceBranch,
"hwlab.pikastech.local/gitops-branch": spec.gitopsBranch,
"hwlab.pikastech.local/node": spec.nodeId,
"hwlab.pikastech.local/network-profile": spec.networkProfileId,
"hwlab.pikastech.local/download-profile": spec.downloadProfileId,
"hwlab.pikastech.local/no-proxy-required": hwlabRequiredNoProxyEntries().join(","),
"hwlab.pikastech.local/triggered-by": "unidesk-cli",
},
},
@@ -3607,6 +3611,9 @@ function runtimeLaneControlPlaneStatus(spec: HwlabRuntimeLaneSpec, target: V02Co
sourceCommit,
expected: {
sourceRepo: spec.cicdRepo,
configPath: hwlabRuntimeLaneConfigPath(),
node: spec.nodeId,
nodeRoute: spec.nodeRoute,
workspace: spec.workspace,
branch: spec.sourceBranch,
namespace: CI_NAMESPACE,
@@ -3616,6 +3623,14 @@ function runtimeLaneControlPlaneStatus(spec: HwlabRuntimeLaneSpec, target: V02Co
argoApplication: spec.app,
gitopsBranch: spec.gitopsBranch,
runtimePath: spec.runtimePath,
networkProfile: {
id: spec.networkProfileId,
httpProxy: spec.networkProfile.proxy.http,
noProxy: spec.networkProfile.proxy.noProxy.join(","),
dockerBuildHttpProxy: spec.networkProfile.dockerBuildProxy.http,
dockerBuildNoProxy: spec.networkProfile.dockerBuildProxy.noProxy.join(","),
},
downloadProfile: spec.downloadProfile,
},
sourceHead: {
ok: shellSectionOk(sections.sourceHead),
@@ -8470,6 +8485,7 @@ export function hwlabG14Help(): Record<string, unknown> {
repo: HWLAB_REPO,
base: G14_SOURCE_BRANCH,
v02Base: V02_SOURCE_BRANCH,
runtimeLaneConfig: hwlabRuntimeLaneConfigPath(),
runtimeLanes: hwlabRuntimeLaneIds(),
provider: G14_PROVIDER,
workspace: G14_WORKSPACE,
@@ -8480,6 +8496,10 @@ export function hwlabG14Help(): Record<string, unknown> {
devApplication: DEV_APP,
v02Application: V02_APP,
v03Application: hwlabRuntimeLaneSpec("v03").app,
v03Node: hwlabRuntimeLaneSpec("v03").nodeId,
v03NetworkProfile: hwlabRuntimeLaneSpec("v03").networkProfileId,
v03DownloadProfile: hwlabRuntimeLaneSpec("v03").downloadProfileId,
requiredNoProxy: hwlabRequiredNoProxyEntries(),
briefIndexIssue: G14_BRIEF_INDEX_ISSUE,
observabilityNamespace: G14_OBSERVABILITY_NAMESPACE,
prometheusOperatorVersion: G14_PROMETHEUS_OPERATOR_VERSION,