feat: add JD01 YAML-first deployment support

This commit is contained in:
Codex
2026-06-29 08:13:34 +00:00
parent fe917bec4a
commit 076c4b643d
49 changed files with 10909 additions and 5223 deletions
+33 -1
View File
@@ -468,6 +468,15 @@ export function nodeRuntimeSync(scoped: ReturnType<typeof parseNodeScopedDelegat
},
};
const patchB64 = Buffer.from(JSON.stringify(operation), "utf8").toString("base64");
const staleOperationStatusPatch = {
status: {
operationState: {
phase: "Failed",
message: "terminated by unidesk control-plane sync stale-operation recovery",
},
},
};
const staleOperationStatusPatchB64 = Buffer.from(JSON.stringify(staleOperationStatusPatch), "utf8").toString("base64");
const script = [
"set +e",
`app=${shellQuote(spec.app)}`,
@@ -475,23 +484,38 @@ export function nodeRuntimeSync(scoped: ReturnType<typeof parseNodeScopedDelegat
`runtime_namespace=${shellQuote(spec.runtimeNamespace)}`,
`dry_run=${shellQuote(scoped.dryRun ? "true" : "false")}`,
`patch_b64=${shellQuote(patchB64)}`,
`stale_operation_status_patch_b64=${shellQuote(staleOperationStatusPatchB64)}`,
"terminate_patch_file=$(mktemp /tmp/hwlab-node-argocd-terminate.XXXXXX.json)",
"stale_operation_status_patch_file=$(mktemp /tmp/hwlab-node-argocd-stale-operation-status.XXXXXX.json)",
"patch_file=$(mktemp /tmp/hwlab-node-argocd-sync.XXXXXX.json)",
"printf '{\"operation\":null}' > \"$terminate_patch_file\"",
"printf '%s' \"$stale_operation_status_patch_b64\" | base64 -d > \"$stale_operation_status_patch_file\"",
"printf '%s' \"$patch_b64\" | base64 -d > \"$patch_file\"",
"before=$(kubectl -n \"$argo_namespace\" get application \"$app\" -o 'jsonpath={.status.sync.status}{\"\\t\"}{.status.health.status}{\"\\t\"}{.status.sync.revision}{\"\\t\"}{.status.operationState.phase}{\"\\t\"}{.status.operationState.message}' 2>/dev/null || true)",
"operation_phase=$(kubectl -n \"$argo_namespace\" get application \"$app\" -o 'jsonpath={.status.operationState.phase}' 2>/dev/null || true)",
"terminate_code=0",
"terminate_output=",
"terminated_operation=false",
"post_terminate_phase=",
"stale_status_patch_code=0",
"stale_status_patch_output=",
"patched_stale_operation_status=false",
"if [ \"$operation_phase\" = Running ]; then",
" if [ \"$dry_run\" = true ]; then",
" terminated_operation=would-terminate",
" patched_stale_operation_status=would-patch",
" else",
" terminate_output=$(kubectl -n \"$argo_namespace\" patch application \"$app\" --type merge --patch-file \"$terminate_patch_file\" -o name 2>&1)",
" terminate_code=$?",
" if [ \"$terminate_code\" -eq 0 ]; then terminated_operation=true; else terminated_operation=failed; fi",
" sleep 2",
" post_terminate_phase=$(kubectl -n \"$argo_namespace\" get application \"$app\" -o 'jsonpath={.status.operationState.phase}' 2>/dev/null || true)",
" if [ \"$terminate_code\" -eq 0 ] && [ \"$post_terminate_phase\" = Running ]; then",
" stale_status_patch_output=$(kubectl -n \"$argo_namespace\" patch application \"$app\" --type merge --patch-file \"$stale_operation_status_patch_file\" -o name 2>&1)",
" stale_status_patch_code=$?",
" if [ \"$stale_status_patch_code\" -eq 0 ]; then patched_stale_operation_status=true; else patched_stale_operation_status=failed; fi",
" sleep 1",
" fi",
" fi",
"fi",
"failed_hook_count=0",
@@ -570,6 +594,10 @@ export function nodeRuntimeSync(scoped: ReturnType<typeof parseNodeScopedDelegat
"printf 'terminatedOperation\\t%s\\n' \"$terminated_operation\"",
"printf 'terminateExitCode\\t%s\\n' \"$terminate_code\"",
"printf 'terminateOutput\\t%s\\n' \"$(printf '%s' \"$terminate_output\" | tr '\\n\\t' ' ' | cut -c1-500)\"",
"printf 'postTerminatePhase\\t%s\\n' \"$post_terminate_phase\"",
"printf 'patchedStaleOperationStatus\\t%s\\n' \"$patched_stale_operation_status\"",
"printf 'staleStatusPatchExitCode\\t%s\\n' \"$stale_status_patch_code\"",
"printf 'staleStatusPatchOutput\\t%s\\n' \"$(printf '%s' \"$stale_status_patch_output\" | tr '\\n\\t' ' ' | cut -c1-500)\"",
"printf 'failedHookCount\\t%s\\n' \"$failed_hook_count\"",
"printf 'failedHooks\\t%s\\n' \"$failed_hooks\"",
"printf 'deletedHookCount\\t%s\\n' \"$deleted_hook_count\"",
@@ -582,7 +610,7 @@ export function nodeRuntimeSync(scoped: ReturnType<typeof parseNodeScopedDelegat
"printf 'staleStatefulSetPodDeleteErrors\\t%s\\n' \"$stale_statefulset_pod_delete_errors\"",
"printf 'patchExitCode\\t%s\\n' \"$code\"",
"printf 'patchOutput\\t%s\\n' \"$(printf '%s' \"$output\" | tr '\\n\\t' ' ' | cut -c1-500)\"",
"rm -f \"$patch_file\" \"$terminate_patch_file\"",
"rm -f \"$patch_file\" \"$terminate_patch_file\" \"$stale_operation_status_patch_file\"",
"exit \"$code\"",
].join("\n");
const result = runNodeK3sScript(spec, script, scoped.timeoutSeconds);
@@ -608,6 +636,10 @@ export function nodeRuntimeSync(scoped: ReturnType<typeof parseNodeScopedDelegat
terminatedOperation: fields.terminatedOperation || "false",
terminateExitCode: numericField(fields.terminateExitCode),
terminateOutput: fields.terminateOutput || null,
postTerminatePhase: fields.postTerminatePhase || null,
patchedStaleOperationStatus: fields.patchedStaleOperationStatus || "false",
staleStatusPatchExitCode: numericField(fields.staleStatusPatchExitCode),
staleStatusPatchOutput: fields.staleStatusPatchOutput || null,
failedHookCount: numericField(fields.failedHookCount),
failedHooks: commaListField(fields.failedHooks),
deletedHookCount: numericField(fields.deletedHookCount),
+24
View File
@@ -306,8 +306,14 @@ export interface NodeSecretOptions {
export interface BootstrapAdminSecretMaterial {
ok: boolean;
username: string | null;
usernameSourceRef: string | null;
usernameSourceKey: string | null;
usernameSourceLine: number | null;
usernameFingerprint: string | null;
sourceRef: string | null;
sourceKey: string | null;
sourceLine: number | null;
sourcePath: string | null;
sourcePresent: boolean;
sourceFingerprint: string | null;
@@ -317,8 +323,14 @@ export interface BootstrapAdminSecretMaterial {
export interface BootstrapAdminPasswordMaterial {
ok: boolean;
username: string | null;
usernameSourceRef: string | null;
usernameSourceKey: string | null;
usernameSourceLine: number | null;
usernameFingerprint: string | null;
sourceRef: string | null;
sourceKey: string | null;
sourceLine: number | null;
sourcePath: string | null;
sourcePresent: boolean;
sourceFingerprint: string | null;
@@ -372,8 +384,12 @@ export interface RuntimeSecretSpec {
bootstrapAdminPasswordHashKey: string;
bootstrapAdminUsername: string;
bootstrapAdminDisplayName: string;
bootstrapAdminUsernameSourceRef?: string;
bootstrapAdminUsernameSourceKey?: string;
bootstrapAdminUsernameSourceLine?: number;
bootstrapAdminPasswordSourceRef?: string;
bootstrapAdminPasswordSourceKey?: string;
bootstrapAdminPasswordSourceLine?: number;
bootstrapAdminPasswordHashTransform?: "hwlab-sha256";
bootstrapAdminSourceNamespace: string;
bootstrapAdminSourceSecret: string;
@@ -442,6 +458,14 @@ export type NodeRuntimeGitMirrorGithubTransportSpec =
export type NodeRuntimeGitMirrorEgressProxySpec =
| { mode: "direct"; required: false }
| {
mode: "host-route";
clientName: string;
hostProxyConfigRef: string;
proxyEnvPath: string;
proxyUrl: string;
noProxy: string[];
}
| {
mode: "k8s-service-cluster-ip";
clientName: string;
+1 -1
View File
@@ -256,7 +256,7 @@ export function nodeObservabilityPerformanceSummary(options: NodeObservabilityOp
};
}
const timeoutSeconds = Math.max(10, Math.min(options.timeoutSeconds, 55));
const script = nodePerformanceSummaryRemoteScript(options.spec.publicWebUrl, summaryPath, secretSpec.bootstrapAdminUsername, material.password);
const script = nodePerformanceSummaryRemoteScript(options.spec.publicWebUrl, summaryPath, material.username ?? secretSpec.bootstrapAdminUsername, material.password);
const result = runTransWorkspaceStdinScript(options.node, options.spec.workspace, script, timeoutSeconds);
const report = parseJsonRecordFromText(result.stdout);
const performance = record(report.performance);
+2
View File
@@ -43,6 +43,7 @@ export function parseNodeScopedDelegatedOptions(domain: DelegatedNodeDomain, arg
dryRun: boolean;
wait: boolean;
rerun: boolean;
discardStaleGitops: boolean;
allowLiveDbRead: boolean;
timeoutSeconds: number;
originalArgs: string[];
@@ -69,6 +70,7 @@ export function parseNodeScopedDelegatedOptions(domain: DelegatedNodeDomain, arg
dryRun,
wait: args.includes("--wait"),
rerun: args.includes("--rerun"),
discardStaleGitops: args.includes("--discard-stale-gitops"),
allowLiveDbRead: args.includes("--allow-live-db-read"),
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", NODE_RUNTIME_CICD_WAIT_WARNING_SECONDS, 3600),
originalArgs: [...args],
@@ -228,8 +228,12 @@ export function runtimeSecretSpec(input: { node: string; lane: string }): Runtim
bootstrapAdminPasswordHashKey: bootstrapAdmin?.secretKey ?? BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY,
bootstrapAdminUsername: bootstrapAdmin?.username ?? "admin",
bootstrapAdminDisplayName: bootstrapAdmin?.displayName ?? `HWLAB ${input.lane} Admin`,
...(bootstrapAdmin?.usernameSourceRef === undefined ? {} : { bootstrapAdminUsernameSourceRef: bootstrapAdmin.usernameSourceRef }),
...(bootstrapAdmin?.usernameSourceKey === undefined ? {} : { bootstrapAdminUsernameSourceKey: bootstrapAdmin.usernameSourceKey }),
...(bootstrapAdmin?.usernameSourceLine === undefined ? {} : { bootstrapAdminUsernameSourceLine: bootstrapAdmin.usernameSourceLine }),
...(bootstrapAdmin?.passwordSourceRef === undefined ? {} : { bootstrapAdminPasswordSourceRef: bootstrapAdmin.passwordSourceRef }),
...(bootstrapAdmin?.passwordSourceKey === undefined ? {} : { bootstrapAdminPasswordSourceKey: bootstrapAdmin.passwordSourceKey }),
...(bootstrapAdmin?.passwordSourceLine === undefined ? {} : { bootstrapAdminPasswordSourceLine: bootstrapAdmin.passwordSourceLine }),
...(bootstrapAdmin?.passwordHashTransform === undefined ? {} : { bootstrapAdminPasswordHashTransform: bootstrapAdmin.passwordHashTransform }),
bootstrapAdminSourceNamespace: BOOTSTRAP_ADMIN_SOURCE_NAMESPACE,
bootstrapAdminSourceSecret: BOOTSTRAP_ADMIN_SOURCE_SECRET,
+129 -5
View File
@@ -45,7 +45,12 @@ export function nodeRuntimeGitMirrorJobName(mirror: NodeRuntimeGitMirrorTargetSp
return `${prefix}-${Date.now().toString(36)}`.slice(0, 63);
}
export function nodeRuntimeGitMirrorJobManifest(mirror: NodeRuntimeGitMirrorTargetSpec, action: "sync" | "flush", jobName: string): Record<string, unknown> {
export function nodeRuntimeGitMirrorJobManifest(
mirror: NodeRuntimeGitMirrorTargetSpec,
action: "sync" | "flush",
jobName: string,
options: { discardStaleGitops?: boolean } = {},
): Record<string, unknown> {
const volumes: Record<string, unknown>[] = [
{ name: "cache", ...(mirror.cacheHostPath === null ? { persistentVolumeClaim: { claimName: mirror.cachePvcName } } : { hostPath: { path: mirror.cacheHostPath, type: "DirectoryOrCreate" } }) },
{ name: "script", configMap: { name: mirror.syncConfigMapName, defaultMode: 0o755 } },
@@ -88,6 +93,7 @@ export function nodeRuntimeGitMirrorJobManifest(mirror: NodeRuntimeGitMirrorTarg
},
},
spec: {
...(mirror.egressProxy.mode === "host-route" ? { hostNetwork: true, dnsPolicy: "ClusterFirstWithHostNet" } : {}),
restartPolicy: "Never",
volumes,
containers: [{
@@ -95,7 +101,11 @@ export function nodeRuntimeGitMirrorJobManifest(mirror: NodeRuntimeGitMirrorTarg
image: mirror.toolsImage,
imagePullPolicy: mirror.toolsImagePullPolicy,
command: [action === "sync" ? "/script/sync.sh" : "/script/flush.sh"],
env: [...nodeRuntimeGitMirrorProxyEnv(mirror), ...nodeRuntimeGitMirrorGithubTransportEnv(mirror)],
env: [
...nodeRuntimeGitMirrorProxyEnv(mirror),
...nodeRuntimeGitMirrorGithubTransportEnv(mirror),
...(options.discardStaleGitops === true ? [{ name: "UNIDESK_GIT_MIRROR_DISCARD_STALE_GITOPS", value: "true" }] : []),
],
volumeMounts,
}],
},
@@ -107,7 +117,7 @@ export function nodeRuntimeGitMirrorJobManifest(mirror: NodeRuntimeGitMirrorTarg
export function nodeRuntimeGitMirrorProxyEnv(mirror: NodeRuntimeGitMirrorTargetSpec): Record<string, unknown>[] {
const proxy = mirror.egressProxy;
if (proxy.mode === "direct") return [];
const proxyUrl = `http://${proxy.serviceName}.${proxy.namespace}.svc.cluster.local:${proxy.port}`;
const proxyUrl = proxy.mode === "host-route" ? proxy.proxyUrl : `http://${proxy.serviceName}.${proxy.namespace}.svc.cluster.local:${proxy.port}`;
const noProxy = proxy.noProxy.join(",");
return [
{ name: "HTTP_PROXY", value: proxyUrl },
@@ -1101,7 +1111,7 @@ export function renderNodeRuntimeControlPlaneOnNode(spec: HwlabRuntimeLaneSpec,
"};",
"if (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;",
"if (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;",
"if (overlay.gitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.gitMirror;",
"if (overlay.deployYamlGitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.deployYamlGitMirror;",
"fs.writeFileSync(path, YAML.stringify(doc));",
"NODE",
"if [ -f scripts/gitops-render.mjs ]; then render_script=scripts/gitops-render.mjs; else echo 'render script missing: scripts/gitops-render.mjs' >&2; exit 43; fi",
@@ -1186,7 +1196,7 @@ export function renderNodeRuntimeControlPlaneLocal(spec: HwlabRuntimeLaneSpec, s
"};",
"if (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;",
"if (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;",
"if (overlay.gitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.gitMirror;",
"if (overlay.deployYamlGitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.deployYamlGitMirror;",
"fs.writeFileSync(path, YAML.stringify(doc));",
"NODE",
"if [ -f scripts/gitops-render.mjs ]; then render_script=scripts/gitops-render.mjs; else echo 'render script missing: scripts/gitops-render.mjs' >&2; exit 43; fi",
@@ -1283,6 +1293,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
" runtimeStore: overlay.runtimeStore,",
" codeAgentRuntime: overlay.codeAgentRuntime,",
" observability: overlay.observability,",
" deployYamlGitMirror: overlay.deployYamlGitMirror,",
" runtimeImageRewrites: overlay.runtimeImageRewrites,",
" dockerProxyHttp: overlay.dockerProxyHttp,",
" dockerProxyHttps: overlay.dockerProxyHttps,",
@@ -1332,6 +1343,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
"};",
"if (overlay.runtimeStore !== undefined) doc.lanes[overlay.lane].runtimeStore = overlay.runtimeStore;",
"if (overlay.codeAgentRuntime !== undefined) doc.lanes[overlay.lane].codeAgentRuntime = overlay.codeAgentRuntime;",
"if (overlay.deployYamlGitMirror !== undefined) doc.lanes[overlay.lane].gitMirror = overlay.deployYamlGitMirror;",
"fs.writeFileSync(file, YAML.stringify(doc));",
"console.error(JSON.stringify({ event: 'unidesk-deploy-yaml-overlay', ok: true, lane: overlay.lane, httpProxy: overlay.dockerProxyHttp, noProxyCount: overlay.dockerNoProxyList.length }));",
"NODE_UNIDESK_DEPLOY_YAML_OVERLAY`;",
@@ -2200,6 +2212,117 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
" if (!changed) throw new Error(`generated git-mirror ConfigMap ${configMapName} was not found in ${gitMirrorFile}`);",
" fs.writeFileSync(gitMirrorFile, docs.map((doc) => YAML.stringify(doc).trimEnd()).join('\\n---\\n') + '\\n');",
"}",
"function patchGitMirrorHostRouteYaml() {",
" const mirror = overlay.gitMirror || {};",
" const proxy = mirror.egressProxy || {};",
" if (proxy.mode !== 'host-route') return;",
" if (!YAML) throw new Error('yaml module is required to patch git-mirror host-route proxy');",
" const requireString = (pathName, value) => {",
" if (typeof value !== 'string' || value.length === 0) throw new Error('overlay.' + pathName + ' is required for git-mirror host-route proxy');",
" return value;",
" };",
" const proxyUrl = requireString('gitMirror.egressProxy.proxyUrl', proxy.proxyUrl);",
" const configMapName = requireString('gitMirror.syncConfigMapName', mirror.syncConfigMapName);",
" let proxyEndpoint;",
" try { proxyEndpoint = new URL(proxyUrl); } catch (error) { throw new Error(`overlay.gitMirror.egressProxy.proxyUrl is invalid: ${error.message}`); }",
" if (proxyEndpoint.protocol !== 'http:') throw new Error('overlay.gitMirror.egressProxy.proxyUrl must use http:// for host-route');",
" const proxyHost = proxyEndpoint.hostname;",
" const proxyPort = Number(proxyEndpoint.port || '80');",
" if (!proxyHost || !Number.isInteger(proxyPort) || proxyPort < 1 || proxyPort > 65535) throw new Error('overlay.gitMirror.egressProxy.proxyUrl must include a valid host and port');",
" const noProxy = Array.isArray(proxy.noProxy) ? proxy.noProxy.join(',') : '';",
" const envEntries = [",
" ['HTTP_PROXY', proxyUrl],",
" ['HTTPS_PROXY', proxyUrl],",
" ['ALL_PROXY', proxyUrl],",
" ['http_proxy', proxyUrl],",
" ['https_proxy', proxyUrl],",
" ['all_proxy', proxyUrl],",
" ['NO_PROXY', noProxy],",
" ['no_proxy', noProxy],",
" ];",
" function readDocs(file) { return YAML.parseAllDocuments(fs.readFileSync(file, 'utf8')).map((document) => document.toJS()).filter((doc) => doc !== null); }",
" function flattenDocs(docs) {",
" const manifests = [];",
" for (const doc of docs) {",
" if (doc && typeof doc === 'object' && doc.kind === 'List' && Array.isArray(doc.items)) manifests.push(...doc.items);",
" else manifests.push(doc);",
" }",
" return manifests;",
" }",
" function setContainerEnv(container, name, value) {",
" if (!container || typeof container !== 'object') return false;",
" container.env = Array.isArray(container.env) ? container.env : [];",
" let item = container.env.find((env) => env && env.name === name);",
" if (!item) { item = { name }; container.env.push(item); }",
" const changed = item.value !== value || item.valueFrom !== undefined;",
" item.value = value;",
" delete item.valueFrom;",
" return changed;",
" }",
" function patchProxyScript(script, key) {",
" let next = String(script || '');",
" if (next.length === 0) throw new Error(`generated git-mirror ConfigMap ${configMapName} missing ${key}`);",
" next = next.replace(/node \\/tmp\\/hwlab-github-proxy-connect\\.(?:mjs|cjs) \\S+ \\d+/g, `node /tmp/hwlab-github-proxy-connect.mjs ${proxyHost} ${proxyPort}`);",
" const missing = [];",
" for (const [name, value] of envEntries) {",
" let replaced = false;",
" const exportPattern = new RegExp(`(^|\\\\n)([ \\\\t]*export ${escapeRegExp(name)}=)[^\\\\n]*`, 'g');",
" next = next.replace(exportPattern, (_match, prefix, left) => { replaced = true; return `${prefix}${left}${shellSingle(value)}`; });",
" if (!replaced) missing.push([name, value]);",
" }",
" if (missing.length > 0) {",
" const block = missing.map(([name, value]) => `export ${name}=${shellSingle(value)}`).join('\\\\n');",
" if (next.includes('\\\\nset -eu\\\\n')) next = next.replace('\\\\nset -eu\\\\n', `\\\\nset -eu\\\\n${block}\\\\n`);",
" else next = `${block}\\\\n${next}`;",
" }",
" next = next.replace(/mode=node-global/g, 'mode=host-route');",
" return next;",
" }",
" const gitMirrorFile = path.join(renderDir, 'devops-infra', 'git-mirror.yaml');",
" if (!fs.existsSync(gitMirrorFile)) throw new Error(`generated git-mirror manifest missing: ${gitMirrorFile}`);",
" const docs = readDocs(gitMirrorFile);",
" const manifests = flattenDocs(docs);",
" let configMapChanged = false;",
" let workloadChanged = false;",
" const workloadNames = new Set([mirror.serviceReadName, mirror.serviceWriteName].filter((value) => typeof value === 'string' && value.length > 0));",
" for (const doc of manifests) {",
" if (!doc || typeof doc !== 'object') continue;",
" if (doc.kind === 'ConfigMap' && doc.metadata && doc.metadata.name === configMapName) {",
" doc.data = doc.data || {};",
" doc.data['sync.sh'] = patchProxyScript(doc.data['sync.sh'], 'sync.sh');",
" doc.data['flush.sh'] = patchProxyScript(doc.data['flush.sh'], 'flush.sh');",
" configMapChanged = true;",
" continue;",
" }",
" if (!['Deployment', 'StatefulSet', 'DaemonSet', 'ReplicaSet', 'ReplicationController'].includes(doc.kind)) continue;",
" const name = doc.metadata && doc.metadata.name ? String(doc.metadata.name) : '';",
" const labels = doc.metadata && doc.metadata.labels && typeof doc.metadata.labels === 'object' ? doc.metadata.labels : {};",
" if (!workloadNames.has(name) && labels['app.kubernetes.io/name'] !== 'git-mirror' && !name.includes('git-mirror')) continue;",
" doc.spec = doc.spec || {};",
" doc.spec.template = doc.spec.template || {};",
" doc.spec.template.metadata = doc.spec.template.metadata || {};",
" doc.spec.template.metadata.annotations = doc.spec.template.metadata.annotations || {};",
" doc.spec.template.metadata.annotations['unidesk.ai/git-mirror-egress-proxy'] = 'host-route';",
" doc.spec.template.metadata.annotations['unidesk.ai/git-mirror-host-proxy-config-ref'] = String(proxy.hostProxyConfigRef || '');",
" doc.spec.template.spec = doc.spec.template.spec || {};",
" if (proxy.podHostNetwork) {",
" doc.spec.template.spec.hostNetwork = true;",
" doc.spec.template.spec.dnsPolicy = 'ClusterFirstWithHostNet';",
" } else {",
" delete doc.spec.template.spec.hostNetwork;",
" delete doc.spec.template.spec.dnsPolicy;",
" }",
" for (const group of ['containers', 'initContainers']) {",
" for (const container of Array.isArray(doc.spec.template.spec[group]) ? doc.spec.template.spec[group] : []) {",
" for (const [envName, envValue] of envEntries) setContainerEnv(container, envName, envValue);",
" }",
" }",
" workloadChanged = true;",
" }",
" if (!configMapChanged) throw new Error(`generated git-mirror ConfigMap ${configMapName} was not found in ${gitMirrorFile}`);",
" if (!workloadChanged) throw new Error(`generated git-mirror workload for host-route proxy was not found in ${gitMirrorFile}`);",
" fs.writeFileSync(gitMirrorFile, docs.map((doc) => YAML.stringify(doc).trimEnd()).join('\\n---\\n') + '\\n');",
"}",
"const structured = patchStructuredPipeline();",
"function replaceParamDefault(name, value) {",
" const namePattern = escapeRegExp(name);",
@@ -2253,6 +2376,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
"if (text.includes('/yaml/-/yaml-') || text.includes('bun add --no-save --ignore-scripts') || text.includes('npm install --package-lock=false --no-save')) { throw new Error(`generated pipeline still downloads yaml during prepare-source in ${pipelinePath}`); }",
"if (text.includes('npm run gitops:ts:check')) { throw new Error(`generated pipeline still uses npm gitops:ts:check gate in ${pipelinePath}`); }",
"fs.writeFileSync(pipelinePath, text);",
"patchGitMirrorHostRouteYaml();",
"patchGitMirrorTransportYaml();",
"function patchArgoYaml(filePath) {",
" if (!YAML || !fs.existsSync(filePath)) return;",
+31 -11
View File
@@ -292,28 +292,48 @@ export function runNodeHostScriptAsync(spec: HwlabRuntimeLaneSpec, script: strin
return commandResultFromAsync(spec, payload, statusPath, false);
}
}
const kill = runNodeHostScript(spec, [
const pending = runNodeHostScript(spec, [
"set +e",
`status_path=${shellQuote(statusPath)}`,
"pid=$(python3 - \"$status_path\" <<'PY' 2>/dev/null",
"import json, sys",
"try: print(json.load(open(sys.argv[1])).get('pid') or '')",
"except Exception: print('')",
`stdout_path=${shellQuote(stdoutPath)}`,
`stderr_path=${shellQuote(stderrPath)}`,
`timeout_seconds=${shellQuote(String(timeoutSeconds))}`,
"python3 - \"$status_path\" \"$stdout_path\" \"$stderr_path\" \"$timeout_seconds\" <<'PY'",
"import datetime, json, pathlib, subprocess, sys",
"status_path, stdout_path, stderr_path, timeout_seconds = sys.argv[1:5]",
"try:",
" payload = json.load(open(status_path))",
"except Exception:",
" payload = {'state': 'missing', 'ok': False, 'exitCode': 127}",
"def tail(path):",
" try:",
" return pathlib.Path(path).read_text(errors='replace')[-12000:]",
" except FileNotFoundError:",
" return ''",
"pid = payload.get('pid')",
"pid_alive = None",
"if isinstance(pid, int) and pid > 0:",
" pid_alive = subprocess.run(['kill', '-0', str(pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0",
"payload['stdout'] = tail(stdout_path)",
"stderr = tail(stderr_path)",
"pending_note = f\"remote async command still pending after {timeout_seconds}s; statusPath={status_path}; pidAlive={pid_alive}\"",
"payload['stderr'] = '\\n'.join([item for item in [pending_note, stderr] if item])",
"payload['timedOutAt'] = datetime.datetime.utcnow().isoformat() + 'Z'",
"payload['pidAlive'] = pid_alive",
"json.dump(payload, sys.stdout, indent=2)",
"PY",
")",
"if [ -n \"$pid\" ]; then kill \"$pid\" 2>/dev/null || true; fi",
"cat \"$status_path\" 2>/dev/null || true",
].join("\n"), 55);
payload = parseJsonObject(pending.stdout.trim());
return {
command: [transPath(), spec.nodeRoute, "sh", "--", "<remote async script>"],
cwd: repoRoot,
exitCode: 124,
stdout: typeof payload.stdout === "string" ? payload.stdout : "",
stdout: typeof payload.stdout === "string" ? payload.stdout : pending.stdout,
stderr: [
`remote async command timed out after ${timeoutSeconds}s; statusPath=${statusPath}`,
`remote async command pending after ${timeoutSeconds}s; statusPath=${statusPath}`,
typeof payload.stderr === "string" ? payload.stderr : "",
lastStatus?.stderr.trim() ?? "",
kill.stderr.trim(),
pending.stderr.trim(),
].filter(Boolean).join("\n"),
signal: null,
timedOut: true,
+55 -3
View File
@@ -1051,10 +1051,15 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
`namespace=${shellQuote(spec.namespace)}`,
`name=${shellQuote(spec.bootstrapAdminSecret)}`,
`password_hash_key=${shellQuote(spec.bootstrapAdminPasswordHashKey)}`,
`username=${shellQuote(spec.bootstrapAdminUsername)}`,
`username=${shellQuote(material?.username ?? spec.bootstrapAdminUsername)}`,
`display_name=${shellQuote(spec.bootstrapAdminDisplayName)}`,
`username_source_ref=${shellQuote(material?.usernameSourceRef ?? "")}`,
`username_source_key=${shellQuote(material?.usernameSourceKey ?? "")}`,
`username_source_line=${shellQuote(material?.usernameSourceLine === null || material?.usernameSourceLine === undefined ? "" : String(material.usernameSourceLine))}`,
`username_source_fingerprint=${shellQuote(material?.usernameFingerprint ?? "")}`,
`source_ref=${shellQuote(spec.bootstrapAdminPasswordSourceRef ?? "")}`,
`source_key=${shellQuote(spec.bootstrapAdminPasswordSourceKey ?? "")}`,
`source_line=${shellQuote(material?.sourceLine === null || material?.sourceLine === undefined ? "" : String(material.sourceLine))}`,
`source_path=${shellQuote(material?.sourcePath === null || material?.sourcePath === undefined ? "" : displayRepoPath(material.sourcePath))}`,
`source_present=${shellQuote(material?.sourcePresent === true ? "yes" : "no")}`,
`source_fingerprint=${shellQuote(material?.sourceFingerprint ?? "")}`,
@@ -1075,8 +1080,13 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
"before_hash_b64=$(secret_b64_key \"$password_hash_key\")",
"before_source_ref=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-ref)",
"before_source_key=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-key)",
"before_source_line=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-line)",
"before_source_fingerprint=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-fingerprint)",
"before_username=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username)",
"before_username_source_ref=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-ref)",
"before_username_source_key=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-key)",
"before_username_source_line=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-line)",
"before_username_source_fingerprint=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-fingerprint)",
"before_hash_present=$([ -n \"$before_hash_b64\" ] && printf yes || printf no)",
"before_hash_bytes=$(decoded_length \"$before_hash_b64\")",
"action=observed",
@@ -1087,7 +1097,8 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
"if [ \"$action_request\" = ensure ]; then",
" needs_sync=false",
" [ \"$before_exists\" = yes ] && [ \"$before_hash_bytes\" -gt 0 ] || needs_sync=true",
" [ \"$before_source_ref\" = \"$source_ref\" ] && [ \"$before_source_key\" = \"$source_key\" ] && [ \"$before_source_fingerprint\" = \"$source_fingerprint\" ] && [ \"$before_username\" = \"$username\" ] || needs_sync=true",
" [ \"$before_source_ref\" = \"$source_ref\" ] && [ \"$before_source_key\" = \"$source_key\" ] && [ \"$before_source_line\" = \"$source_line\" ] && [ \"$before_source_fingerprint\" = \"$source_fingerprint\" ] && [ \"$before_username\" = \"$username\" ] || needs_sync=true",
" [ \"$before_username_source_ref\" = \"$username_source_ref\" ] && [ \"$before_username_source_key\" = \"$username_source_key\" ] && [ \"$before_username_source_line\" = \"$username_source_line\" ] && [ \"$before_username_source_fingerprint\" = \"$username_source_fingerprint\" ] || needs_sync=true",
" [ \"$force_sync\" = true ] && needs_sync=true",
" if [ \"$material_ok\" != true ]; then",
" action=${source_error:-secret-source-invalid}",
@@ -1109,8 +1120,13 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
" annotations:",
" hwlab.pikastech.local/bootstrap-admin-username: \"$username\"",
" hwlab.pikastech.local/bootstrap-admin-display-name: \"$display_name\"",
" hwlab.pikastech.local/bootstrap-admin-username-source-ref: \"$username_source_ref\"",
" hwlab.pikastech.local/bootstrap-admin-username-source-key: \"$username_source_key\"",
" hwlab.pikastech.local/bootstrap-admin-username-source-line: \"$username_source_line\"",
" hwlab.pikastech.local/bootstrap-admin-username-source-fingerprint: \"$username_source_fingerprint\"",
" hwlab.pikastech.local/bootstrap-admin-source-ref: \"$source_ref\"",
" hwlab.pikastech.local/bootstrap-admin-source-key: \"$source_key\"",
" hwlab.pikastech.local/bootstrap-admin-source-line: \"$source_line\"",
" hwlab.pikastech.local/bootstrap-admin-source-fingerprint: \"$source_fingerprint\"",
" hwlab.pikastech.local/bootstrap-admin-password-transform: \"$transform\"",
" labels:",
@@ -1140,8 +1156,13 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
"after_hash_b64=$(secret_b64_key \"$password_hash_key\")",
"after_source_ref=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-ref)",
"after_source_key=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-key)",
"after_source_line=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-line)",
"after_source_fingerprint=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-source-fingerprint)",
"after_username=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username)",
"after_username_source_ref=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-ref)",
"after_username_source_key=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-key)",
"after_username_source_line=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-line)",
"after_username_source_fingerprint=$(secret_annotation hwlab.pikastech.local/bootstrap-admin-username-source-fingerprint)",
"after_hash_present=$([ -n \"$after_hash_b64\" ] && printf yes || printf no)",
"after_hash_bytes=$(decoded_length \"$after_hash_b64\")",
"printf 'namespace\\t%s\\n' \"$namespace\"",
@@ -1150,8 +1171,13 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
"printf 'preset\\t%s\\n' \"$preset\"",
"printf 'username\\t%s\\n' \"$username\"",
"printf 'displayName\\t%s\\n' \"$display_name\"",
"printf 'usernameSourceRef\\t%s\\n' \"$username_source_ref\"",
"printf 'usernameSourceKey\\t%s\\n' \"$username_source_key\"",
"printf 'usernameSourceLine\\t%s\\n' \"$username_source_line\"",
"printf 'usernameSourceFingerprint\\t%s\\n' \"$username_source_fingerprint\"",
"printf 'sourceRef\\t%s\\n' \"$source_ref\"",
"printf 'sourceKey\\t%s\\n' \"$source_key\"",
"printf 'sourceLine\\t%s\\n' \"$source_line\"",
"printf 'sourcePath\\t%s\\n' \"$source_path\"",
"printf 'sourceExists\\t%s\\n' \"$source_present\"",
"printf 'sourceFingerprint\\t%s\\n' \"$source_fingerprint\"",
@@ -1165,15 +1191,25 @@ export function bootstrapAdminSecretScript(options: NodeSecretOptions, spec: Run
"printf 'beforePasswordHashBytes\\t%s\\n' \"$before_hash_bytes\"",
"printf 'beforeSourceRef\\t%s\\n' \"$before_source_ref\"",
"printf 'beforeSourceKey\\t%s\\n' \"$before_source_key\"",
"printf 'beforeSourceLine\\t%s\\n' \"$before_source_line\"",
"printf 'beforeSourceFingerprint\\t%s\\n' \"$before_source_fingerprint\"",
"printf 'beforeUsername\\t%s\\n' \"$before_username\"",
"printf 'beforeUsernameSourceRef\\t%s\\n' \"$before_username_source_ref\"",
"printf 'beforeUsernameSourceKey\\t%s\\n' \"$before_username_source_key\"",
"printf 'beforeUsernameSourceLine\\t%s\\n' \"$before_username_source_line\"",
"printf 'beforeUsernameSourceFingerprint\\t%s\\n' \"$before_username_source_fingerprint\"",
"printf 'afterExists\\t%s\\n' \"$after_exists\"",
"printf 'afterPasswordHashPresent\\t%s\\n' \"$after_hash_present\"",
"printf 'afterPasswordHashBytes\\t%s\\n' \"$after_hash_bytes\"",
"printf 'afterSourceRef\\t%s\\n' \"$after_source_ref\"",
"printf 'afterSourceKey\\t%s\\n' \"$after_source_key\"",
"printf 'afterSourceLine\\t%s\\n' \"$after_source_line\"",
"printf 'afterSourceFingerprint\\t%s\\n' \"$after_source_fingerprint\"",
"printf 'afterUsername\\t%s\\n' \"$after_username\"",
"printf 'afterUsernameSourceRef\\t%s\\n' \"$after_username_source_ref\"",
"printf 'afterUsernameSourceKey\\t%s\\n' \"$after_username_source_key\"",
"printf 'afterUsernameSourceLine\\t%s\\n' \"$after_username_source_line\"",
"printf 'afterUsernameSourceFingerprint\\t%s\\n' \"$after_username_source_fingerprint\"",
"printf 'cloudApiDeployment\\t%s\\n' \"$cloud_api_deployment\"",
"printf 'applyExitCode\\t%s\\n' \"$apply_exit\"",
"printf 'rolloutRestartExitCode\\t%s\\n' \"$rollout_restart_exit\"",
@@ -1759,8 +1795,13 @@ export function secretStatusFromText(text: string, commandOk: boolean, exitCode:
fields.sourceFingerprint.length > 0 &&
fields.afterSourceRef === fields.sourceRef &&
fields.afterSourceKey === fields.sourceKey &&
(fields.afterSourceLine || "") === (fields.sourceLine || "") &&
fields.afterSourceFingerprint === fields.sourceFingerprint &&
fields.afterUsername === fields.username
fields.afterUsername === fields.username &&
(fields.afterUsernameSourceRef || "") === (fields.usernameSourceRef || "") &&
(fields.afterUsernameSourceKey || "") === (fields.usernameSourceKey || "") &&
(fields.afterUsernameSourceLine || "") === (fields.usernameSourceLine || "") &&
(fields.afterUsernameSourceFingerprint || "") === (fields.usernameSourceFingerprint || "")
);
const healthy = targetHashReady && yamlSourceReady;
return {
@@ -1777,6 +1818,7 @@ export function secretStatusFromText(text: string, commandOk: boolean, exitCode:
? {
sourceRef: fields.sourceRef,
sourceKey: fields.sourceKey || null,
sourceLine: numericField(fields.sourceLine),
sourcePath: fields.sourcePath || null,
exists: fields.sourceExists === "yes",
fingerprint: fields.sourceFingerprint || null,
@@ -1799,8 +1841,13 @@ export function secretStatusFromText(text: string, commandOk: boolean, exitCode:
...(yamlSourceMode ? {
sourceRef: fields.beforeSourceRef || null,
sourceKey: fields.beforeSourceKey || null,
sourceLine: numericField(fields.beforeSourceLine),
sourceFingerprint: fields.beforeSourceFingerprint || null,
username: fields.beforeUsername || null,
usernameSourceRef: fields.beforeUsernameSourceRef || null,
usernameSourceKey: fields.beforeUsernameSourceKey || null,
usernameSourceLine: numericField(fields.beforeUsernameSourceLine),
usernameSourceFingerprint: fields.beforeUsernameSourceFingerprint || null,
} : {}),
},
after: {
@@ -1809,8 +1856,13 @@ export function secretStatusFromText(text: string, commandOk: boolean, exitCode:
...(yamlSourceMode ? {
sourceRef: fields.afterSourceRef || null,
sourceKey: fields.afterSourceKey || null,
sourceLine: numericField(fields.afterSourceLine),
sourceFingerprint: fields.afterSourceFingerprint || null,
username: fields.afterUsername || null,
usernameSourceRef: fields.afterUsernameSourceRef || null,
usernameSourceKey: fields.afterUsernameSourceKey || null,
usernameSourceLine: numericField(fields.afterUsernameSourceLine),
usernameSourceFingerprint: fields.afterUsernameSourceFingerprint || null,
} : {}),
},
cloudApiDeployment: fields.cloudApiDeployment || spec.cloudApiDeployment,
+52 -28
View File
@@ -117,7 +117,14 @@ export function nodeRuntimeGitMirrorStatus(scoped: ReturnType<typeof parseNodeSc
"PY",
")",
"summary_json=$(kubectl -n \"$namespace\" exec deploy/\"$read_deploy\" -- sh -lc '/etc/git-mirror/status.sh' 2>/tmp/hwlab-node-gitmirror-status.err || true)",
"if ! printf '%s' \"$summary_json\" | node -e 'let s=\"\"; process.stdin.on(\"data\", c => s += c); process.stdin.on(\"end\", () => { try { const o = JSON.parse(s || \"{}\"); process.exit(o && o.localSource ? 0 : 1); } catch { process.exit(1); } });'; then",
"if ! SUMMARY_JSON=\"$summary_json\" python3 - <<'PY'; then",
"import json, os, sys",
"try:",
" value = json.loads(os.environ.get('SUMMARY_JSON') or '{}')",
"except Exception:",
" value = {}",
"sys.exit(0 if isinstance(value, dict) and value.get('localSource') else 1)",
"PY",
" summary_json=$(kubectl -n \"$namespace\" exec deploy/\"$read_deploy\" -- sh -lc \"source_repository=\\$1 source_branch=\\$2 gitops_branch=\\$3 node <<'NODE'",
"const { execFileSync } = require('node:child_process');",
"const { readFileSync, existsSync } = require('node:fs');",
@@ -161,29 +168,37 @@ export function nodeRuntimeGitMirrorStatus(scoped: ReturnType<typeof parseNodeSc
"cache_pvc_exists=$(exists_res \"$namespace\" pvc \"$cache_pvc\")",
"cache_host_path_exists=false",
"if [ -n \"$cache_host_path\" ] && kubectl -n \"$namespace\" exec deploy/\"$read_deploy\" -- sh -lc 'test -d /cache' >/dev/null 2>&1; then cache_host_path_exists=true; fi",
"SUMMARY_JSON=\"$summary_json\" GITHUB_TRANSPORT_JSON=\"$github_transport_json\" read_deployment_ready=\"$read_deployment_ready\" write_deployment_ready=\"$write_deployment_ready\" read_service_exists=\"$read_service_exists\" write_service_exists=\"$write_service_exists\" read_endpoints_ready=\"$read_endpoints_ready\" write_endpoints_ready=\"$write_endpoints_ready\" cache_pvc_exists=\"$cache_pvc_exists\" cache_host_path=\"$cache_host_path\" cache_host_path_exists=\"$cache_host_path_exists\" node <<'NODE'",
"const summary = (() => { try { return JSON.parse(process.env.SUMMARY_JSON || '{}'); } catch { return {}; } })();",
"const githubTransport = (() => { try { return JSON.parse(process.env.GITHUB_TRANSPORT_JSON || '{}'); } catch { return {}; } })();",
"const env = process.env;",
"const ok = env.read_deployment_ready === 'true' && env.write_deployment_ready === 'true' && env.read_service_exists === 'true' && env.write_service_exists === 'true' && env.read_endpoints_ready === 'true' && env.write_endpoints_ready === 'true' && (env.cache_pvc_exists === 'true' || env.cache_host_path_exists === 'true') && githubTransport.ready !== false && summary.localSource;",
"console.log(JSON.stringify({",
" ok: Boolean(ok),",
" resources: {",
" readDeploymentReady: env.read_deployment_ready === 'true',",
" writeDeploymentReady: env.write_deployment_ready === 'true',",
" readServiceExists: env.read_service_exists === 'true',",
" writeServiceExists: env.write_service_exists === 'true',",
" readEndpointsReady: env.read_endpoints_ready === 'true',",
" writeEndpointsReady: env.write_endpoints_ready === 'true',",
" cachePvcExists: env.cache_pvc_exists === 'true',",
" cacheHostPathConfigured: Boolean(env.cache_host_path),",
" cacheHostPathExists: env.cache_host_path_exists === 'true'",
" },",
" githubTransport,",
" summary,",
" valuesPrinted: false",
"}));",
"NODE",
"SUMMARY_JSON=\"$summary_json\" GITHUB_TRANSPORT_JSON=\"$github_transport_json\" read_deployment_ready=\"$read_deployment_ready\" write_deployment_ready=\"$write_deployment_ready\" read_service_exists=\"$read_service_exists\" write_service_exists=\"$write_service_exists\" read_endpoints_ready=\"$read_endpoints_ready\" write_endpoints_ready=\"$write_endpoints_ready\" cache_pvc_exists=\"$cache_pvc_exists\" cache_host_path=\"$cache_host_path\" cache_host_path_exists=\"$cache_host_path_exists\" python3 - <<'PY'",
"import json, os",
"def load_env_json(name):",
" try:",
" value = json.loads(os.environ.get(name) or '{}')",
" return value if isinstance(value, dict) else {}",
" except Exception:",
" return {}",
"def truth(name):",
" return os.environ.get(name) == 'true'",
"summary = load_env_json('SUMMARY_JSON')",
"github_transport = load_env_json('GITHUB_TRANSPORT_JSON')",
"ok = truth('read_deployment_ready') and truth('write_deployment_ready') and truth('read_service_exists') and truth('write_service_exists') and truth('read_endpoints_ready') and truth('write_endpoints_ready') and (truth('cache_pvc_exists') or truth('cache_host_path_exists')) and github_transport.get('ready') is not False and bool(summary.get('localSource'))",
"print(json.dumps({",
" 'ok': bool(ok),",
" 'resources': {",
" 'readDeploymentReady': truth('read_deployment_ready'),",
" 'writeDeploymentReady': truth('write_deployment_ready'),",
" 'readServiceExists': truth('read_service_exists'),",
" 'writeServiceExists': truth('write_service_exists'),",
" 'readEndpointsReady': truth('read_endpoints_ready'),",
" 'writeEndpointsReady': truth('write_endpoints_ready'),",
" 'cachePvcExists': truth('cache_pvc_exists'),",
" 'cacheHostPathConfigured': bool(os.environ.get('cache_host_path')),",
" 'cacheHostPathExists': truth('cache_host_path_exists'),",
" },",
" 'githubTransport': github_transport,",
" 'summary': summary,",
" 'valuesPrinted': False,",
"}))",
"PY",
].join("\n");
const result = runNodeK3sScript(spec, script, scoped.timeoutSeconds);
const parsed = record(parseJsonObject(statusText(result)));
@@ -240,7 +255,9 @@ export function nodeRuntimeGitMirrorRun(scoped: ReturnType<typeof parseNodeScope
for (let attempt = 1; attempt <= retryMaxAttempts; attempt += 1) {
const retryLabel = `${attempt}/${retryMaxAttempts}`;
const jobName = nodeRuntimeGitMirrorJobName(mirror, scoped.action);
const manifest = nodeRuntimeGitMirrorJobManifest(mirror, scoped.action, jobName);
const manifest = nodeRuntimeGitMirrorJobManifest(mirror, scoped.action, jobName, {
discardStaleGitops: scoped.discardStaleGitops === true,
});
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
const waitTimeoutSeconds = Math.max(5, Math.min(45, Math.max(5, scoped.timeoutSeconds - 10)));
const script = [
@@ -429,7 +446,7 @@ export function nodeRuntimeGitMirrorRetryableFailure(
: waitTimeoutFailure
? "Git mirror job wait exceeded the controlled short-connection budget. Standard git-mirror retries with exponential backoff and stops when retry budget is exhausted."
: proxyConnectFailure || sshProxyBannerFailure
? "Git mirror job hit a retryable YAML-first SSH-over-proxy failure. Standard git-mirror keeps using the configured node-global proxy and stops when retry budget is exhausted."
? "Git mirror job hit a retryable YAML-first SSH-over-proxy failure. Standard git-mirror keeps using the configured proxy and stops when retry budget is exhausted."
: `Git mirror job hit a retryable upstream GitHub ${mirror.githubTransport.mode} transport/fetch failure. Standard git-mirror stops without host workspace fallback.`,
refSources: nodeRuntimeGitMirrorRefSources(scoped, mirror),
githubTransport: nodeRuntimeGitMirrorGithubTransportSummary(mirror),
@@ -489,7 +506,15 @@ export function nodeRuntimeEnsureGitMirrorSourceCurrent(scoped: ReturnType<typeo
degradedReason: flush.ok === true ? undefined : "node-runtime-git-mirror-pre-flush-failed",
};
}
const sync = nodeRuntimeGitMirrorRun({ ...scoped, domain: "git-mirror", action: "sync", confirm: true, dryRun: false, wait: true });
const sync = nodeRuntimeGitMirrorRun({
...scoped,
domain: "git-mirror",
action: "sync",
confirm: true,
dryRun: false,
wait: true,
discardStaleGitops: scoped.discardStaleGitops === true || scoped.rerun === true,
});
const after = record(sync.status);
const afterSummary = Object.keys(after).length > 0 ? compactNodeRuntimeGitMirrorStatus(after) : {};
const sourceOk = sync.ok === true && afterSummary.localSource === sourceCommit && afterSummary.githubSource === sourceCommit;
@@ -633,7 +658,6 @@ export function nodeRuntimeGitMirrorNeedsFlush(status: Record<string, unknown>):
const githubGitops = typeof summary.githubGitops === "string" ? summary.githubGitops : null;
return summary.pendingFlush === true
|| summary.flushNeeded === true
|| summary.githubInSync === false
|| (localGitops !== null && githubGitops !== null && localGitops !== githubGitops);
}
@@ -470,7 +470,7 @@ export function runNodeWebProbeScript(
credential: Record<string, unknown>,
): Record<string, unknown> {
const webProbeProxy = nodeWebProbeHostProxyEnv(spec, options.browserProxyMode);
const script = nodeWebProbeScriptRemoteShell(options, secretSpec, material.password ?? "", webProbeProxy);
const script = nodeWebProbeScriptRemoteShell(options, secretSpec, material.username ?? secretSpec.bootstrapAdminUsername, material.password ?? "", webProbeProxy);
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const commandTimedOut = result.timedOut || result.exitCode === 124;
const stdoutReport = parseJsonObject(result.stdout);
@@ -606,7 +606,7 @@ function webProbeScriptPreferredCommands(options: NodeWebProbeScriptOptions): Re
};
}
export function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions, secretSpec: RuntimeSecretSpec, password: string, webProbeProxy: NodeWebProbeHostProxyEnv): string {
export function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions, secretSpec: RuntimeSecretSpec, username: string, password: string, webProbeProxy: NodeWebProbeHostProxyEnv): string {
const userScriptB64 = Buffer.from(options.scriptText, "utf8").toString("base64");
const runnerB64 = Buffer.from(nodeWebProbeScriptRunnerSource(), "utf8").toString("base64");
return [
@@ -625,7 +625,7 @@ export function nodeWebProbeScriptRemoteShell(options: NodeWebProbeScriptOptions
[
...webProbeProxy.envAssignments,
`HWLAB_WEB_BASE_URL=${shellQuote(options.url)}`,
`HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`,
`HWLAB_WEB_USER=${shellQuote(username)}`,
`HWLAB_WEB_PASS=${shellQuote(password)}`,
"UNIDESK_WEB_PROBE_RUN_DIR=\"$run_dir\"",
"UNIDESK_WEB_PROBE_USER_SCRIPT=\"$user_script\"",
+8 -4
View File
@@ -582,7 +582,7 @@ export function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, un
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
const script = [
"set -eu",
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password)}`].join(" ")} ${probeArgs.map(shellQuote).join(" ")}`,
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(material.username ?? secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password)}`].join(" ")} ${probeArgs.map(shellQuote).join(" ")}`,
].join("\n");
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
const probe = compactWebProbeResult(parseJsonObject(result.stdout));
@@ -922,7 +922,7 @@ export function runNodeWebProbeAsync(
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
const startScript = [
"set -eu",
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password ?? "")}`].join(" ")} ${startArgs.map(shellQuote).join(" ")}`,
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(material.username ?? secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password ?? "")}`].join(" ")} ${startArgs.map(shellQuote).join(" ")}`,
].join("\n");
const startResult = runTransWorkspaceStdinScript(options.node, spec.workspace, startScript, 55);
const start = parseJsonObject(startResult.stdout);
@@ -1114,7 +1114,11 @@ export function webProbeCommandTimeoutSummary(options: NodeWebProbeRunOptions, t
export function webProbeCredential(secretSpec: RuntimeSecretSpec, material: BootstrapAdminPasswordMaterial): Record<string, unknown> {
return {
username: secretSpec.bootstrapAdminUsername,
username: material.username === null ? secretSpec.bootstrapAdminUsername : "<source-ref>",
usernameSourceRef: material.usernameSourceRef,
usernameSourceKey: material.usernameSourceKey,
usernameSourceLine: material.usernameSourceLine,
usernameFingerprint: material.usernameFingerprint,
sourceRef: material.sourceRef,
sourceKey: material.sourceKey,
sourcePath: material.sourcePath === null ? null : displayRepoPath(material.sourcePath),
@@ -1310,7 +1314,7 @@ export function runNodeWebProbeObserveStart(
...webProbeProxy.envAssignments,
...webProbeAccountEnvAssignments(),
`HWLAB_WEB_BASE_URL=${shellQuote(options.url)}`,
`HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`,
`HWLAB_WEB_USER=${shellQuote(material.username ?? secretSpec.bootstrapAdminUsername)}`,
`HWLAB_WEB_PASS=${shellQuote(material.password)}`,
`UNIDESK_WEB_OBSERVE_STATE_DIR=${shellQuote(stateDir)}`,
`UNIDESK_WEB_OBSERVE_JOB_ID=${shellQuote(jobId)}`,
+153 -34
View File
@@ -49,12 +49,28 @@ export function nodeRuntimeRenderOverlay(spec: HwlabRuntimeLaneSpec): Record<str
egressProxy: gitMirror.egressProxy.mode === "direct" ? {
mode: "direct",
required: false,
} : gitMirror.egressProxy.mode === "host-route" ? {
...gitMirror.egressProxy,
mode: "host-route",
required: true,
} : {
...gitMirror.egressProxy,
mode: "node-global",
required: true,
},
};
const deployYamlGitMirror = {
...renderGitMirror,
egressProxy: renderGitMirror.egressProxy.mode !== "host-route" ? renderGitMirror.egressProxy : {
mode: "node-global",
required: true,
clientName: renderGitMirror.egressProxy.clientName,
namespace: "platform-infra",
serviceName: renderGitMirror.egressProxy.clientName,
port: httpProxyEndpoint(renderGitMirror.egressProxy.proxyUrl)?.port ?? 10808,
noProxy: renderGitMirror.egressProxy.noProxy,
},
};
return {
nodeId: spec.nodeId,
lane: spec.lane,
@@ -75,6 +91,7 @@ export function nodeRuntimeRenderOverlay(spec: HwlabRuntimeLaneSpec): Record<str
toolsImage: gitMirror.toolsImage,
toolsImagePullPolicy: gitMirror.toolsImagePullPolicy,
gitMirror: renderGitMirror,
deployYamlGitMirror,
networkProfileId: spec.networkProfileId,
downloadProfileId: spec.downloadProfileId,
gitSshProxyHost: gitSshProxy?.host,
@@ -704,6 +721,7 @@ export function readLocalPostgresPasswordMaterial(input: { sourceRef: string; so
}
export function localSecretSourcePaths(sourceRef: string): string[] {
if (sourceRef.startsWith(".env/")) return ownerFileSourcePaths(sourceRef);
const marker = "/.worktree/";
const index = repoRoot.indexOf(marker);
const paths = index >= 0
@@ -1034,6 +1052,16 @@ export function ensureNodeBaseImage(spec: HwlabRuntimeLaneSpec, dryRun: boolean,
" if [ \"$mode\" = build-moonbridge ]; then",
" action=build",
" source_present_before=build-source",
" effective_build_container_http_proxy=\"$build_container_http_proxy\"",
" effective_build_container_https_proxy=\"$build_container_https_proxy\"",
" effective_build_container_all_proxy=\"$build_container_all_proxy\"",
" effective_docker_build_add_host_args=\"$docker_build_add_host_args\"",
" if [ \"$docker_network_mode\" = host ]; then",
" effective_build_container_http_proxy=\"$build_http_proxy\"",
" effective_build_container_https_proxy=\"$build_https_proxy\"",
" effective_build_container_all_proxy=\"$build_all_proxy\"",
" effective_docker_build_add_host_args=\"\"",
" fi",
" tmpdir=$(mktemp -d /tmp/hwlab-node-runtime-image-$id.XXXXXX)",
" dockerfile=\"$tmpdir/Dockerfile\"",
" cat > \"$dockerfile\" <<'DOCKERFILE'",
@@ -1069,7 +1097,7 @@ export function ensureNodeBaseImage(spec: HwlabRuntimeLaneSpec, dryRun: boolean,
"USER 65532:65532",
"ENTRYPOINT [\"/app/moonbridge\"]",
"DOCKERFILE",
" if env HTTP_PROXY=\"$build_http_proxy\" HTTPS_PROXY=\"$build_https_proxy\" ALL_PROXY=\"$build_all_proxy\" NO_PROXY=\"$build_no_proxy\" http_proxy=\"$build_http_proxy\" https_proxy=\"$build_https_proxy\" all_proxy=\"$build_all_proxy\" no_proxy=\"$build_no_proxy\" docker build $docker_build_add_host_args --network \"$docker_network_mode\" --build-arg BUILDER_IMAGE=\"$builder_image\" --build-arg MOONBRIDGE_REPO=\"$source_repo\" --build-arg MOONBRIDGE_REF=\"$source_ref\" --build-arg GOPROXY_VALUE=\"$go_proxy\" --build-arg HTTP_PROXY=\"$build_container_http_proxy\" --build-arg HTTPS_PROXY=\"$build_container_https_proxy\" --build-arg ALL_PROXY=\"$build_container_all_proxy\" --build-arg NO_PROXY=\"$build_no_proxy\" --build-arg http_proxy=\"$build_container_http_proxy\" --build-arg https_proxy=\"$build_container_https_proxy\" --build-arg all_proxy=\"$build_container_all_proxy\" --build-arg no_proxy=\"$build_no_proxy\" -t \"$target\" -f \"$dockerfile\" \"$tmpdir\" >/tmp/hwlab-node-runtime-image-$id-build.out 2>&1; then",
" if env HTTP_PROXY=\"$build_http_proxy\" HTTPS_PROXY=\"$build_https_proxy\" ALL_PROXY=\"$build_all_proxy\" NO_PROXY=\"$build_no_proxy\" http_proxy=\"$build_http_proxy\" https_proxy=\"$build_https_proxy\" all_proxy=\"$build_all_proxy\" no_proxy=\"$build_no_proxy\" docker build $effective_docker_build_add_host_args --network \"$docker_network_mode\" --build-arg BUILDER_IMAGE=\"$builder_image\" --build-arg MOONBRIDGE_REPO=\"$source_repo\" --build-arg MOONBRIDGE_REF=\"$source_ref\" --build-arg GOPROXY_VALUE=\"$go_proxy\" --build-arg HTTP_PROXY=\"$effective_build_container_http_proxy\" --build-arg HTTPS_PROXY=\"$effective_build_container_https_proxy\" --build-arg ALL_PROXY=\"$effective_build_container_all_proxy\" --build-arg NO_PROXY=\"$build_no_proxy\" --build-arg http_proxy=\"$effective_build_container_http_proxy\" --build-arg https_proxy=\"$effective_build_container_https_proxy\" --build-arg all_proxy=\"$effective_build_container_all_proxy\" --build-arg no_proxy=\"$build_no_proxy\" -t \"$target\" -f \"$dockerfile\" \"$tmpdir\" >/tmp/hwlab-node-runtime-image-$id-build.out 2>&1; then",
" docker push \"$target\" >/tmp/hwlab-node-runtime-image-$id-push.out 2>&1 || { cat /tmp/hwlab-node-runtime-image-$id-push.out >&2 2>/dev/null || true; failed=true; }",
" else",
" cat /tmp/hwlab-node-runtime-image-$id-build.out >&2 2>/dev/null || true",
@@ -1122,9 +1150,12 @@ export function ensureNodeBaseImage(spec: HwlabRuntimeLaneSpec, dryRun: boolean,
? runNodeHostScriptAsync(spec, script, Math.min(timeoutSeconds, 600), "runtime-image-build")
: runNodeHostScript(spec, script, Math.min(timeoutSeconds, 300));
const imageRows = parseNodeRuntimeImageRows(statusText(result));
const dependencyCount = nodeRuntimeImageDependencies(spec).length;
const imageRowsComplete = imageRows.length === dependencyCount
&& imageRows.every((image) => image.presentAfter === true || image.registryTagPresent === true);
const base = imageRows.find((image) => image.id === "base") ?? {};
return {
ok: isCommandSuccess(result),
ok: isCommandSuccess(result) || imageRowsComplete,
dryRun,
target: base.target ?? spec.baseImage,
source: base.source ?? spec.baseImageSource,
@@ -1196,6 +1227,7 @@ export function readSecretSourceValue(secretRoot: string, sourceRef: string, key
}
export function secretSourcePaths(sourceRef: string): string[] {
if (sourceRef.startsWith(".env/")) return ownerFileSourcePaths(sourceRef);
const paths = [join(repoRoot, ".state", "secrets", sourceRef)];
const marker = "/.worktree/";
const index = repoRoot.indexOf(marker);
@@ -1203,6 +1235,14 @@ export function secretSourcePaths(sourceRef: string): string[] {
return [...new Set(paths)];
}
function ownerFileSourcePaths(sourceRef: string): string[] {
if (sourceRef.includes("..") || sourceRef.includes("\0")) return [];
const marker = "/.worktree/";
const index = repoRoot.indexOf(marker);
const roots = index >= 0 ? [repoRoot.slice(0, index), repoRoot] : [repoRoot];
return [...new Set(roots.map((root) => join(root, sourceRef)))];
}
export function displayRepoPath(path: string): string {
const normalizedRoot = repoRoot.replace(/\/+$/u, "");
if (path === normalizedRoot) return ".";
@@ -1225,27 +1265,28 @@ export function hwlabPasswordHash(password: string): string {
export function readBootstrapAdminSecretMaterial(spec: RuntimeSecretSpec): BootstrapAdminSecretMaterial {
const sourceRef = spec.bootstrapAdminPasswordSourceRef;
const sourceKey = spec.bootstrapAdminPasswordSourceKey;
const sourceLine = spec.bootstrapAdminPasswordSourceLine ?? null;
if (sourceRef === undefined || sourceKey === undefined || spec.bootstrapAdminPasswordHashTransform === undefined) {
return { ok: false, sourceRef: sourceRef ?? null, sourceKey: sourceKey ?? null, sourcePath: null, sourcePresent: false, sourceFingerprint: null, passwordHash: null, error: "bootstrap-admin-yaml-source-missing" };
}
const paths = secretSourcePaths(sourceRef);
const sourcePath = paths.find((candidate) => existsSync(candidate)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
if (!existsSync(sourcePath)) {
return { ok: false, sourceRef, sourceKey, sourcePath, sourcePresent: false, sourceFingerprint: null, passwordHash: null, error: "secret-source-missing" };
}
const values = parseEnvFile(readFileSync(sourcePath, "utf8"));
const password = values[sourceKey];
if (password === undefined || password.length === 0) {
return { ok: false, sourceRef, sourceKey, sourcePath, sourcePresent: true, sourceFingerprint: null, passwordHash: null, error: "secret-key-missing" };
return bootstrapAdminSecretMaterialError(spec, sourceRef ?? null, sourceKey ?? null, sourceLine, null, null, "bootstrap-admin-yaml-source-missing");
}
const password = readSecretSourceScalar(sourceRef, sourceKey, sourceLine);
if (!password.ok) return bootstrapAdminSecretMaterialError(spec, sourceRef, sourceKey, sourceLine, password.sourcePath, password.sourcePresent, password.error);
const username = readBootstrapAdminUsername(spec);
if (!username.ok) return bootstrapAdminSecretMaterialError(spec, sourceRef, sourceKey, sourceLine, password.sourcePath, password.sourcePresent, username.error);
return {
ok: true,
username: username.value,
usernameSourceRef: spec.bootstrapAdminUsernameSourceRef ?? null,
usernameSourceKey: spec.bootstrapAdminUsernameSourceKey ?? null,
usernameSourceLine: spec.bootstrapAdminUsernameSourceLine ?? null,
usernameFingerprint: shortSecretFingerprint(username.value),
sourceRef,
sourceKey,
sourcePath,
sourceLine,
sourcePath: password.sourcePath,
sourcePresent: true,
sourceFingerprint: `sha256:${createHash("sha256").update(password).digest("hex").slice(0, 16)}`,
passwordHash: hwlabPasswordHash(password),
sourceFingerprint: shortSecretFingerprint(password.value),
passwordHash: hwlabPasswordHash(password.value),
error: null,
};
}
@@ -1253,32 +1294,96 @@ export function readBootstrapAdminSecretMaterial(spec: RuntimeSecretSpec): Boots
export function readBootstrapAdminPasswordMaterial(spec: RuntimeSecretSpec): BootstrapAdminPasswordMaterial {
const sourceRef = spec.bootstrapAdminPasswordSourceRef;
const sourceKey = spec.bootstrapAdminPasswordSourceKey;
const sourceLine = spec.bootstrapAdminPasswordSourceLine ?? null;
if (sourceRef === undefined || sourceKey === undefined || spec.bootstrapAdminPasswordHashTransform === undefined) {
return { ok: false, sourceRef: sourceRef ?? null, sourceKey: sourceKey ?? null, sourcePath: null, sourcePresent: false, sourceFingerprint: null, password: null, error: "bootstrap-admin-yaml-source-missing" };
}
const paths = secretSourcePaths(sourceRef);
const sourcePath = paths.find((candidate) => existsSync(candidate)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
const runtimePassword = process.env[sourceKey];
if (!existsSync(sourcePath) && (runtimePassword === undefined || runtimePassword.length === 0)) {
return { ok: false, sourceRef, sourceKey, sourcePath, sourcePresent: false, sourceFingerprint: null, password: null, error: "secret-source-missing" };
}
const values = existsSync(sourcePath) ? parseEnvFile(readFileSync(sourcePath, "utf8")) : {};
const password = values[sourceKey] ?? runtimePassword;
if (password === undefined || password.length === 0) {
return { ok: false, sourceRef, sourceKey, sourcePath, sourcePresent: true, sourceFingerprint: null, password: null, error: "secret-key-missing" };
return bootstrapAdminPasswordMaterialError(spec, sourceRef ?? null, sourceKey ?? null, sourceLine, null, null, "bootstrap-admin-yaml-source-missing");
}
const password = readSecretSourceScalar(sourceRef, sourceKey, sourceLine);
if (!password.ok) return bootstrapAdminPasswordMaterialError(spec, sourceRef, sourceKey, sourceLine, password.sourcePath, password.sourcePresent, password.error);
const username = readBootstrapAdminUsername(spec);
if (!username.ok) return bootstrapAdminPasswordMaterialError(spec, sourceRef, sourceKey, sourceLine, password.sourcePath, password.sourcePresent, username.error);
return {
ok: true,
username: username.value,
usernameSourceRef: spec.bootstrapAdminUsernameSourceRef ?? null,
usernameSourceKey: spec.bootstrapAdminUsernameSourceKey ?? null,
usernameSourceLine: spec.bootstrapAdminUsernameSourceLine ?? null,
usernameFingerprint: shortSecretFingerprint(username.value),
sourceRef,
sourceKey,
sourcePath,
sourceLine,
sourcePath: password.sourcePath,
sourcePresent: true,
sourceFingerprint: `sha256:${createHash("sha256").update(password).digest("hex").slice(0, 16)}`,
password,
sourceFingerprint: shortSecretFingerprint(password.value),
password: password.value,
error: null,
};
}
function readBootstrapAdminUsername(spec: RuntimeSecretSpec): { ok: true; value: string } | { ok: false; error: string } {
const ref = spec.bootstrapAdminUsernameSourceRef;
if (ref === undefined) return { ok: true, value: spec.bootstrapAdminUsername };
const key = spec.bootstrapAdminUsernameSourceKey ?? "username";
const material = readSecretSourceScalar(ref, key, spec.bootstrapAdminUsernameSourceLine ?? null);
if (!material.ok) return { ok: false, error: `username-${material.error}` };
return { ok: true, value: material.value };
}
function readSecretSourceScalar(sourceRef: string, sourceKey: string, sourceLine: number | null): { ok: true; value: string; sourcePath: string; sourcePresent: true } | { ok: false; sourcePath: string; sourcePresent: boolean; error: string } {
const paths = secretSourcePaths(sourceRef);
const sourcePath = paths.find((candidate) => existsSync(candidate)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef);
if (!existsSync(sourcePath)) return { ok: false, sourcePath, sourcePresent: false, error: "secret-source-missing" };
const text = readFileSync(sourcePath, "utf8");
if (sourceLine !== null) {
const line = text.split(/\r?\n/u)[sourceLine - 1]?.replace(/\r$/u, "") ?? "";
if (line.length === 0) return { ok: false, sourcePath, sourcePresent: true, error: "secret-line-missing" };
return { ok: true, value: line, sourcePath, sourcePresent: true };
}
const values = parseEnvFile(text);
const runtimeValue = process.env[sourceKey];
const value = values[sourceKey] ?? runtimeValue;
if (value === undefined || value.length === 0) return { ok: false, sourcePath, sourcePresent: true, error: "secret-key-missing" };
return { ok: true, value, sourcePath, sourcePresent: true };
}
function bootstrapAdminSecretMaterialError(spec: RuntimeSecretSpec, sourceRef: string | null, sourceKey: string | null, sourceLine: number | null, sourcePath: string | null, sourcePresent: boolean | null, error: string): BootstrapAdminSecretMaterial {
return {
ok: false,
username: null,
usernameSourceRef: spec.bootstrapAdminUsernameSourceRef ?? null,
usernameSourceKey: spec.bootstrapAdminUsernameSourceKey ?? null,
usernameSourceLine: spec.bootstrapAdminUsernameSourceLine ?? null,
usernameFingerprint: null,
sourceRef,
sourceKey,
sourceLine,
sourcePath,
sourcePresent: sourcePresent === true,
sourceFingerprint: null,
passwordHash: null,
error,
};
}
function bootstrapAdminPasswordMaterialError(spec: RuntimeSecretSpec, sourceRef: string | null, sourceKey: string | null, sourceLine: number | null, sourcePath: string | null, sourcePresent: boolean | null, error: string): BootstrapAdminPasswordMaterial {
return {
ok: false,
username: null,
usernameSourceRef: spec.bootstrapAdminUsernameSourceRef ?? null,
usernameSourceKey: spec.bootstrapAdminUsernameSourceKey ?? null,
usernameSourceLine: spec.bootstrapAdminUsernameSourceLine ?? null,
usernameFingerprint: null,
sourceRef,
sourceKey,
sourceLine,
sourcePath,
sourcePresent: sourcePresent === true,
sourceFingerprint: null,
password: null,
error,
};
}
export function parseEnvFile(text: string): Record<string, string> {
const values: Record<string, string> = {};
for (const rawLine of text.split(/\r?\n/u)) {
@@ -1547,11 +1652,13 @@ export function nodeRuntimeGitMirrorTarget(spec: HwlabRuntimeLaneSpec): NodeRunt
const gitMirror = record(target.gitMirror);
const gitMirrorEgressProxy = record(gitMirror.egressProxy);
const gitMirrorEgressMode = stringValue(gitMirrorEgressProxy.mode, "gitMirror.egressProxy.mode");
if (gitMirrorEgressMode !== "node-global" && gitMirrorEgressMode !== "direct") throw new Error(`gitMirror.egressProxy.mode must be node-global or direct for node=${spec.nodeId} lane=${spec.lane}`);
if (gitMirrorEgressMode !== "node-global" && gitMirrorEgressMode !== "host-route" && gitMirrorEgressMode !== "direct") throw new Error(`gitMirror.egressProxy.mode must be node-global, host-route, or direct for node=${spec.nodeId} lane=${spec.lane}`);
const nodeEgressProxy = gitMirrorEgressMode === "direct"
? { mode: "direct" as const, required: false as const }
: nodeRuntimeGitMirrorEgressProxySpec(record(node.egressProxy), `nodes.${spec.nodeId}.egressProxy`);
if (gitMirrorEgressMode === "node-global" && gitMirrorEgressProxy.required !== true) throw new Error(`gitMirror.egressProxy.required must be true for node=${spec.nodeId} lane=${spec.lane}`);
if ((gitMirrorEgressMode === "node-global" || gitMirrorEgressMode === "host-route") && gitMirrorEgressProxy.required !== true) throw new Error(`gitMirror.egressProxy.required must be true for node=${spec.nodeId} lane=${spec.lane}`);
if (gitMirrorEgressMode === "node-global" && nodeEgressProxy.mode !== "k8s-service-cluster-ip") throw new Error(`gitMirror.egressProxy.mode=node-global requires nodes.${spec.nodeId}.egressProxy.mode=k8s-service-cluster-ip`);
if (gitMirrorEgressMode === "host-route" && nodeEgressProxy.mode !== "host-route") throw new Error(`gitMirror.egressProxy.mode=host-route requires nodes.${spec.nodeId}.egressProxy.mode=host-route`);
const githubTransport = nodeRuntimeGitMirrorGithubTransportSpec(record(gitMirror.githubTransport), "gitMirror.githubTransport");
const source = record(target.source);
const gitops = record(target.gitops);
@@ -1626,7 +1733,19 @@ function gitMirrorSecretSourceEncoding(raw: unknown, path: string): "plain" | "b
export function nodeRuntimeGitMirrorEgressProxySpec(raw: Record<string, unknown>, path: string): NodeRuntimeGitMirrorEgressProxySpec {
const mode = stringValue(raw.mode, `${path}.mode`);
if (mode !== "k8s-service-cluster-ip") throw new Error(`${path}.mode must be k8s-service-cluster-ip`);
if (mode === "host-route") {
const noProxyRaw = raw.noProxy;
if (!Array.isArray(noProxyRaw)) throw new Error(`${path}.noProxy must be an array`);
return {
mode,
clientName: stringValue(raw.clientName, `${path}.clientName`),
hostProxyConfigRef: stringValue(raw.hostProxyConfigRef, `${path}.hostProxyConfigRef`),
proxyEnvPath: stringValue(raw.proxyEnvPath, `${path}.proxyEnvPath`),
proxyUrl: stringValue(raw.proxyUrl, `${path}.proxyUrl`),
noProxy: noProxyRaw.map((item, index) => stringValue(item, `${path}.noProxy[${index}]`)),
};
}
if (mode !== "k8s-service-cluster-ip") throw new Error(`${path}.mode must be k8s-service-cluster-ip or host-route`);
const port = Number(raw.port);
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`${path}.port must be a TCP port`);
const sourceConfigRef = optionalStringValue(raw.sourceConfigRef, `${path}.sourceConfigRef`) ?? null;