ac4de2c1d5
Co-authored-by: Codex <codex@noreply.local>
940 lines
41 KiB
TypeScript
940 lines
41 KiB
TypeScript
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
|
|
// Responsibility: YAML source-of-truth parsing for HWLAB node/lane Workbench observability.
|
|
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 HwlabRuntimePostgresStoreSpec {
|
|
readonly poolMax: number;
|
|
readonly connectionTimeoutMs?: number;
|
|
readonly queryRetryMaxAttempts?: number;
|
|
readonly queryRetryInitialDelayMs?: number;
|
|
readonly queryRetryMaxDelayMs?: number;
|
|
}
|
|
|
|
export interface HwlabRuntimeStoreSpec {
|
|
readonly postgres?: HwlabRuntimePostgresStoreSpec;
|
|
}
|
|
|
|
export interface HwlabRuntimeBuildkitSpec {
|
|
readonly sidecarImage: string;
|
|
}
|
|
|
|
export interface HwlabRuntimeObservabilitySpec {
|
|
readonly prometheusOperator: boolean;
|
|
readonly traceExplorerUrlTemplate?: string;
|
|
readonly metricsEndpoint?: HwlabRuntimeObservabilityMetricsEndpointSpec;
|
|
readonly workbench?: HwlabRuntimeObservabilityWorkbenchSpec;
|
|
readonly recordingRules: readonly HwlabRuntimeObservabilityRecordingRuleSpec[];
|
|
readonly warningAlerts: readonly HwlabRuntimeObservabilityWarningAlertSpec[];
|
|
}
|
|
|
|
export interface HwlabRuntimeObservabilityMetricsEndpointSpec {
|
|
readonly serviceName: string;
|
|
readonly containerName: string;
|
|
readonly port: number;
|
|
readonly scheme: "http";
|
|
readonly path: string;
|
|
readonly scrapeMode: "pod-loopback";
|
|
readonly publicRawMetrics: "denied";
|
|
}
|
|
|
|
export interface HwlabRuntimeObservabilityWorkbenchSpec {
|
|
readonly enabled: boolean;
|
|
readonly summaryPath: string;
|
|
readonly lowSampleThreshold: number;
|
|
readonly metricPrefixes: readonly string[];
|
|
readonly requiredSeries: readonly string[];
|
|
readonly backendLabelDenylist: readonly string[];
|
|
readonly maxUnknownEventLines: number;
|
|
}
|
|
|
|
export interface HwlabRuntimeObservabilityRecordingRuleSpec {
|
|
readonly id: string;
|
|
readonly metric: string;
|
|
readonly sourceMetric: string;
|
|
readonly quantile: number;
|
|
readonly window: string;
|
|
readonly minSamples: number;
|
|
readonly groupBy: readonly string[];
|
|
readonly matchLabels: Record<string, string>;
|
|
}
|
|
|
|
export interface HwlabRuntimeObservabilityWarningAlertSpec {
|
|
readonly id: string;
|
|
readonly ruleId: string;
|
|
readonly severity: "warning";
|
|
readonly thresholdSeconds: number;
|
|
readonly minSamples: number;
|
|
readonly for: string;
|
|
readonly matchLabels: Record<string, string>;
|
|
}
|
|
|
|
export interface HwlabRuntimeImageRewriteSpec {
|
|
readonly source: string;
|
|
readonly target: string;
|
|
}
|
|
|
|
export interface HwlabRuntimePublicExposureFrpcProxySpec {
|
|
readonly name: string;
|
|
readonly localIP: string;
|
|
readonly localPort: number;
|
|
readonly remotePort: number;
|
|
}
|
|
|
|
export interface HwlabRuntimePublicExposureSpec {
|
|
readonly enabled: boolean;
|
|
readonly mode: "pk01-caddy-frp";
|
|
readonly publicBaseUrl: string;
|
|
readonly hostname: string;
|
|
readonly expectedA: string;
|
|
readonly serverAddr: string;
|
|
readonly serverPort: number;
|
|
readonly tokenSourceRef: string;
|
|
readonly tokenSourceKey: string;
|
|
readonly secretName: string;
|
|
readonly secretKey: string;
|
|
readonly tokenKey: string;
|
|
readonly caddyRoute: string;
|
|
readonly caddyConfigPath: string;
|
|
readonly caddyServiceName: string;
|
|
readonly caddyEmail: string;
|
|
readonly caddyTls: "auto" | "internal";
|
|
readonly responseHeaderTimeoutSeconds: number;
|
|
readonly webProxy: HwlabRuntimePublicExposureFrpcProxySpec;
|
|
readonly apiProxy: HwlabRuntimePublicExposureFrpcProxySpec;
|
|
}
|
|
|
|
export interface HwlabRuntimeBootstrapAdminSpec {
|
|
readonly username: string;
|
|
readonly displayName: string;
|
|
readonly passwordSourceRef: string;
|
|
readonly passwordSourceKey: string;
|
|
readonly passwordHashTransform: "hwlab-sha256";
|
|
readonly secretName: string;
|
|
readonly secretKey: string;
|
|
readonly rolloutDeployment: 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<string, string>;
|
|
readonly buildkit?: HwlabRuntimeBuildkitSpec;
|
|
readonly bootstrapAdmin?: HwlabRuntimeBootstrapAdminSpec;
|
|
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
|
|
readonly runtimeStore?: HwlabRuntimeStoreSpec;
|
|
readonly publicExposure: HwlabRuntimePublicExposureSpec | null;
|
|
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<string, string>;
|
|
readonly buildkit?: HwlabRuntimeBuildkitSpec;
|
|
readonly bootstrapAdmin?: HwlabRuntimeBootstrapAdminSpec;
|
|
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
|
|
readonly runtimeStore?: HwlabRuntimeStoreSpec;
|
|
readonly publicExposure: HwlabRuntimePublicExposureSpec | null;
|
|
readonly observability: HwlabRuntimeObservabilitySpec;
|
|
readonly runtimeImageRewrites: readonly HwlabRuntimeImageRewriteSpec[];
|
|
}
|
|
|
|
interface HwlabNodeLaneConfig {
|
|
readonly defaultTarget: {
|
|
readonly node: string;
|
|
readonly lane: HwlabRuntimeLane;
|
|
};
|
|
readonly requiredNoProxy: readonly string[];
|
|
readonly nodes: Record<string, HwlabRuntimeNodeSpec>;
|
|
readonly lanes: Record<HwlabRuntimeLane, HwlabLaneConfig>;
|
|
readonly laneTargets: Partial<Record<HwlabRuntimeLane, Record<string, 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 booleanField(obj: Record<string, unknown>, 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 nonNegativeIntegerField(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 non-negative integer`);
|
|
return value;
|
|
}
|
|
|
|
function positiveNumberField(obj: Record<string, unknown>, key: string, path: string): number {
|
|
const value = obj[key];
|
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new Error(`${path}.${key} must be a positive number`);
|
|
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 mergeOptionalRecord(base: unknown, override: unknown): Record<string, unknown> | 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<string, string> {
|
|
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<string, unknown>, 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<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`),
|
|
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<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 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`),
|
|
bootstrapAdmin: bootstrapAdminConfig(raw.bootstrapAdmin, `lanes.${id}.bootstrapAdmin`),
|
|
externalPostgres: externalPostgresConfig(raw.externalPostgres, `lanes.${id}.externalPostgres`),
|
|
runtimeStore: runtimeStoreConfig(raw.runtimeStore, `lanes.${id}.runtimeStore`),
|
|
publicExposure: publicExposureConfig(raw.publicExposure, `lanes.${id}.publicExposure`),
|
|
observability: observabilityConfig(raw.observability, `lanes.${id}.observability`),
|
|
runtimeImageRewrites: runtimeImageRewritesConfig(raw.runtimeImageRewrites, `lanes.${id}.runtimeImageRewrites`),
|
|
};
|
|
}
|
|
|
|
function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record<string, unknown>, targetRaw: Record<string, unknown>): HwlabLaneConfig {
|
|
const merged: Record<string, unknown> = {
|
|
...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),
|
|
bootstrapAdmin: mergeOptionalRecord(baseRaw.bootstrapAdmin, targetRaw.bootstrapAdmin),
|
|
externalPostgres: mergeOptionalRecord(baseRaw.externalPostgres, targetRaw.externalPostgres),
|
|
runtimeStore: mergeOptionalRecord(baseRaw.runtimeStore, targetRaw.runtimeStore),
|
|
publicExposure: mergeOptionalRecord(baseRaw.publicExposure, targetRaw.publicExposure),
|
|
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 sourceRefField(obj: Record<string, unknown>, key: string, path: string): string {
|
|
const value = stringField(obj, key, path);
|
|
if (!/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${path}.${key} has an unsupported format`);
|
|
return value;
|
|
}
|
|
|
|
function secretKeyField(obj: Record<string, unknown>, key: string, path: string): string {
|
|
const value = stringField(obj, key, path);
|
|
if (!/^[A-Za-z0-9_.-]+$/u.test(value)) throw new Error(`${path}.${key} has an unsupported format`);
|
|
return value;
|
|
}
|
|
|
|
function bootstrapAdminConfig(value: unknown, path: string): HwlabRuntimeBootstrapAdminSpec | undefined {
|
|
if (value === undefined) return undefined;
|
|
const raw = asRecord(value, path);
|
|
const transform = stringField(raw, "passwordHashTransform", path);
|
|
if (transform !== "hwlab-sha256") throw new Error(`${path}.passwordHashTransform must be hwlab-sha256`);
|
|
const rollout = asRecord(raw.rollout, `${path}.rollout`);
|
|
return {
|
|
username: stringField(raw, "username", path),
|
|
displayName: stringField(raw, "displayName", path),
|
|
passwordSourceRef: sourceRefField(raw, "passwordSourceRef", path),
|
|
passwordSourceKey: secretKeyField(raw, "passwordSourceKey", path),
|
|
passwordHashTransform: transform,
|
|
secretName: stringField(raw, "secretName", path),
|
|
secretKey: secretKeyField(raw, "secretKey", path),
|
|
rolloutDeployment: stringField(rollout, "deployment", `${path}.rollout`),
|
|
};
|
|
}
|
|
|
|
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 runtimeStoreConfig(value: unknown, path: string): HwlabRuntimeStoreSpec | undefined {
|
|
if (value === undefined) return undefined;
|
|
const raw = asRecord(value, path);
|
|
const postgres = raw.postgres === undefined ? undefined : asRecord(raw.postgres, `${path}.postgres`);
|
|
return {
|
|
...(postgres === undefined
|
|
? {}
|
|
: {
|
|
postgres: {
|
|
poolMax: numberField(postgres, "poolMax", `${path}.postgres`),
|
|
...(postgres.connectionTimeoutMs === undefined
|
|
? {}
|
|
: { connectionTimeoutMs: numberField(postgres, "connectionTimeoutMs", `${path}.postgres`) }),
|
|
...(postgres.queryRetryMaxAttempts === undefined
|
|
? {}
|
|
: { queryRetryMaxAttempts: numberField(postgres, "queryRetryMaxAttempts", `${path}.postgres`) }),
|
|
...(postgres.queryRetryInitialDelayMs === undefined
|
|
? {}
|
|
: { queryRetryInitialDelayMs: numberField(postgres, "queryRetryInitialDelayMs", `${path}.postgres`) }),
|
|
...(postgres.queryRetryMaxDelayMs === undefined
|
|
? {}
|
|
: { queryRetryMaxDelayMs: numberField(postgres, "queryRetryMaxDelayMs", `${path}.postgres`) }),
|
|
},
|
|
}),
|
|
};
|
|
}
|
|
|
|
function publicExposureProxyConfig(value: unknown, path: string): HwlabRuntimePublicExposureFrpcProxySpec {
|
|
const raw = asRecord(value, path);
|
|
return {
|
|
name: stringField(raw, "name", path),
|
|
localIP: stringField(raw, "localIP", path),
|
|
localPort: numberField(raw, "localPort", path),
|
|
remotePort: numberField(raw, "remotePort", path),
|
|
};
|
|
}
|
|
|
|
function publicExposureConfig(value: unknown, path: string): HwlabRuntimePublicExposureSpec | null {
|
|
if (value === undefined) return null;
|
|
const raw = asRecord(value, path);
|
|
const mode = stringField(raw, "mode", path);
|
|
if (mode !== "pk01-caddy-frp") throw new Error(`${path}.mode must be pk01-caddy-frp`);
|
|
const frpc = asRecord(raw.frpc, `${path}.frpc`);
|
|
const caddy = asRecord(raw.caddy, `${path}.caddy`);
|
|
const caddyTls = optionalStringField(caddy, "tls", `${path}.caddy`) ?? "auto";
|
|
if (caddyTls !== "auto" && caddyTls !== "internal") throw new Error(`${path}.caddy.tls must be auto or internal`);
|
|
return {
|
|
enabled: true,
|
|
mode,
|
|
publicBaseUrl: stringField(raw, "publicBaseUrl", path).replace(/\/+$/u, ""),
|
|
hostname: stringField(raw, "hostname", path),
|
|
expectedA: stringField(raw, "expectedA", path),
|
|
serverAddr: stringField(frpc, "serverAddr", `${path}.frpc`),
|
|
serverPort: numberField(frpc, "serverPort", `${path}.frpc`),
|
|
tokenSourceRef: stringField(frpc, "tokenSourceRef", `${path}.frpc`),
|
|
tokenSourceKey: stringField(frpc, "tokenSourceKey", `${path}.frpc`),
|
|
secretName: stringField(frpc, "secretName", `${path}.frpc`),
|
|
secretKey: optionalStringField(frpc, "secretKey", `${path}.frpc`) ?? "frpc.toml",
|
|
tokenKey: optionalStringField(frpc, "tokenKey", `${path}.frpc`) ?? "token",
|
|
caddyRoute: stringField(caddy, "route", `${path}.caddy`),
|
|
caddyConfigPath: stringField(caddy, "configPath", `${path}.caddy`),
|
|
caddyServiceName: stringField(caddy, "serviceName", `${path}.caddy`),
|
|
caddyEmail: stringField(caddy, "email", `${path}.caddy`),
|
|
caddyTls,
|
|
responseHeaderTimeoutSeconds: numberField(caddy, "responseHeaderTimeoutSeconds", `${path}.caddy`),
|
|
webProxy: publicExposureProxyConfig(frpc.webProxy, `${path}.frpc.webProxy`),
|
|
apiProxy: publicExposureProxyConfig(frpc.apiProxy, `${path}.frpc.apiProxy`),
|
|
};
|
|
}
|
|
|
|
function observabilityConfig(value: unknown, path: string): HwlabRuntimeObservabilitySpec {
|
|
const raw = asRecord(value, path);
|
|
const recordingRules = observabilityRecordingRulesConfig(raw.recordingRules, `${path}.recordingRules`);
|
|
const warningAlerts = observabilityWarningAlertsConfig(raw.warningAlerts, `${path}.warningAlerts`);
|
|
const recordingRuleIds = new Set(recordingRules.map((rule) => rule.id));
|
|
for (const alert of warningAlerts) {
|
|
if (!recordingRuleIds.has(alert.ruleId)) throw new Error(`${path}.warningAlerts.${alert.id}.ruleId must reference a recordingRules id`);
|
|
}
|
|
const traceExplorerUrlTemplate = optionalStringField(raw, "traceExplorerUrlTemplate", path);
|
|
if (traceExplorerUrlTemplate !== undefined) validateTraceExplorerUrlTemplate(traceExplorerUrlTemplate, `${path}.traceExplorerUrlTemplate`);
|
|
return {
|
|
prometheusOperator: booleanField(raw, "prometheusOperator", path),
|
|
traceExplorerUrlTemplate,
|
|
metricsEndpoint: observabilityMetricsEndpointConfig(raw.metricsEndpoint, `${path}.metricsEndpoint`),
|
|
workbench: observabilityWorkbenchConfig(raw.workbench, `${path}.workbench`),
|
|
recordingRules,
|
|
warningAlerts,
|
|
};
|
|
}
|
|
|
|
function validateTraceExplorerUrlTemplate(template: string, path: string): void {
|
|
if (!template.includes("{trace_id}")) throw new Error(`${path} must include {trace_id}`);
|
|
if (template.startsWith("//")) throw new Error(`${path} must not be protocol-relative`);
|
|
const placeholders = Array.from(template.matchAll(/\{([A-Za-z0-9_:-]+)\}/gu), (match) => match[0]);
|
|
if (placeholders.length === 0 || placeholders.some((placeholder) => placeholder !== "{trace_id}")) {
|
|
throw new Error(`${path} may only use the {trace_id} placeholder`);
|
|
}
|
|
try {
|
|
const url = new URL(template.replaceAll("{trace_id}", "trc_template_probe"), "https://hwlab.local");
|
|
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("unsupported protocol");
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
throw new Error(`${path} must be an http(s) or same-origin relative URL template: ${message}`);
|
|
}
|
|
}
|
|
|
|
function observabilityMetricsEndpointConfig(value: unknown, path: string): HwlabRuntimeObservabilityMetricsEndpointSpec | undefined {
|
|
if (value === undefined) return undefined;
|
|
const raw = asRecord(value, path);
|
|
const scheme = stringField(raw, "scheme", path);
|
|
if (scheme !== "http") throw new Error(`${path}.scheme must be http`);
|
|
const scrapeMode = stringField(raw, "scrapeMode", path);
|
|
if (scrapeMode !== "pod-loopback") throw new Error(`${path}.scrapeMode must be pod-loopback`);
|
|
const publicRawMetrics = stringField(raw, "publicRawMetrics", path);
|
|
if (publicRawMetrics !== "denied") throw new Error(`${path}.publicRawMetrics must be denied`);
|
|
return {
|
|
serviceName: stringField(raw, "serviceName", path),
|
|
containerName: stringField(raw, "containerName", path),
|
|
port: numberField(raw, "port", path),
|
|
scheme,
|
|
path: stringField(raw, "path", path),
|
|
scrapeMode,
|
|
publicRawMetrics,
|
|
};
|
|
}
|
|
|
|
function observabilityWorkbenchConfig(value: unknown, path: string): HwlabRuntimeObservabilityWorkbenchSpec | undefined {
|
|
if (value === undefined) return undefined;
|
|
const raw = asRecord(value, path);
|
|
return {
|
|
enabled: booleanField(raw, "enabled", path),
|
|
summaryPath: stringField(raw, "summaryPath", path),
|
|
lowSampleThreshold: numberField(raw, "lowSampleThreshold", path),
|
|
metricPrefixes: stringArrayField(raw, "metricPrefixes", path),
|
|
requiredSeries: stringArrayField(raw, "requiredSeries", path),
|
|
backendLabelDenylist: stringArrayField(raw, "backendLabelDenylist", path),
|
|
maxUnknownEventLines: nonNegativeIntegerField(raw, "maxUnknownEventLines", path),
|
|
};
|
|
}
|
|
|
|
function observabilityRecordingRulesConfig(value: unknown, path: string): HwlabRuntimeObservabilityRecordingRuleSpec[] {
|
|
if (value === undefined) return [];
|
|
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
|
|
return value.map((item, index) => {
|
|
const itemPath = `${path}[${index}]`;
|
|
const raw = asRecord(item, itemPath);
|
|
const quantile = positiveNumberField(raw, "quantile", itemPath);
|
|
if (quantile >= 1) throw new Error(`${itemPath}.quantile must be less than 1`);
|
|
return {
|
|
id: stringField(raw, "id", itemPath),
|
|
metric: stringField(raw, "metric", itemPath),
|
|
sourceMetric: stringField(raw, "sourceMetric", itemPath),
|
|
quantile,
|
|
window: stringField(raw, "window", itemPath),
|
|
minSamples: numberField(raw, "minSamples", itemPath),
|
|
groupBy: stringArrayField(raw, "groupBy", itemPath),
|
|
matchLabels: optionalStringRecord(raw.matchLabels, `${itemPath}.matchLabels`),
|
|
};
|
|
});
|
|
}
|
|
|
|
function observabilityWarningAlertsConfig(value: unknown, path: string): HwlabRuntimeObservabilityWarningAlertSpec[] {
|
|
if (value === undefined) return [];
|
|
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
|
|
return value.map((item, index) => {
|
|
const itemPath = `${path}[${index}]`;
|
|
const raw = asRecord(item, itemPath);
|
|
const severity = stringField(raw, "severity", itemPath);
|
|
if (severity !== "warning") throw new Error(`${itemPath}.severity must be warning`);
|
|
return {
|
|
id: stringField(raw, "id", itemPath),
|
|
ruleId: stringField(raw, "ruleId", itemPath),
|
|
severity,
|
|
thresholdSeconds: positiveNumberField(raw, "thresholdSeconds", itemPath),
|
|
minSamples: numberField(raw, "minSamples", itemPath),
|
|
for: stringField(raw, "for", itemPath),
|
|
matchLabels: optionalStringRecord(raw.matchLabels, `${itemPath}.matchLabels`),
|
|
};
|
|
});
|
|
}
|
|
|
|
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);
|
|
validateConfigEnvelope(parsed);
|
|
const defaultTarget = parseDefaultTarget(asRecord(parsed.defaults, `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults`));
|
|
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<HwlabRuntimeLane, HwlabLaneConfig>;
|
|
const laneTargets: Partial<Record<HwlabRuntimeLane, Record<string, HwlabLaneConfig>>> = {};
|
|
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}`);
|
|
}
|
|
if (nodes[defaultTarget.node] === undefined) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.defaults.node references missing node ${defaultTarget.node}`);
|
|
const defaultLane = laneTargets[defaultTarget.lane]?.[defaultTarget.node] ?? lanes[defaultTarget.lane];
|
|
if (defaultLane === undefined || defaultLane.node !== defaultTarget.node) {
|
|
throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.defaults must reference a declared lane target`);
|
|
}
|
|
return { defaultTarget, requiredNoProxy, nodes, lanes, laneTargets, networkProfiles, downloadProfiles };
|
|
}
|
|
|
|
function validateConfigEnvelope(parsed: Record<string, unknown>): void {
|
|
if (parsed.version !== 1) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.version must be 1`);
|
|
if (parsed.kind !== "HwlabNodeLaneConfig") throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.kind must be HwlabNodeLaneConfig`);
|
|
const metadata = asRecord(parsed.metadata, `${HWLAB_NODE_LANE_CONFIG_PATH}.metadata`);
|
|
if (stringField(metadata, "name", `${HWLAB_NODE_LANE_CONFIG_PATH}.metadata`) !== "hwlab-node-lanes") {
|
|
throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.metadata.name must be hwlab-node-lanes`);
|
|
}
|
|
}
|
|
|
|
function parseDefaultTarget(raw: Record<string, unknown>): { readonly node: string; readonly lane: HwlabRuntimeLane } {
|
|
const lane = stringField(raw, "lane", `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults`);
|
|
if (!isSupportedLaneId(lane)) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.defaults.lane is not supported by this CLI build`);
|
|
return {
|
|
node: stringField(raw, "node", `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults`),
|
|
lane,
|
|
};
|
|
}
|
|
|
|
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.bootstrapAdmin === undefined ? {} : { bootstrapAdmin: config.bootstrapAdmin }),
|
|
...(config.externalPostgres === undefined ? {} : { externalPostgres: config.externalPostgres }),
|
|
...(config.runtimeStore === undefined ? {} : { runtimeStore: config.runtimeStore }),
|
|
publicExposure: config.publicExposure,
|
|
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<HwlabRuntimeLane, HwlabRuntimeLaneSpec>;
|
|
|
|
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 hwlabDefaultRuntimeTarget(): { readonly node: string; readonly lane: HwlabRuntimeLane } {
|
|
return { ...HWLAB_NODE_LANE_CONFIG.defaultTarget };
|
|
}
|
|
|
|
export function hwlabRuntimeLaneConfigPath(): string {
|
|
return HWLAB_NODE_LANE_CONFIG_PATH;
|
|
}
|
|
|
|
export function hwlabRequiredNoProxyEntries(): string[] {
|
|
return [...HWLAB_NODE_LANE_CONFIG.requiredNoProxy];
|
|
}
|