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
+140 -48
View File
@@ -35,10 +35,17 @@ import { sha256Fingerprint } from "../platform-infra-ops-library";
import { runYamlLaneGitMirrorFlushJob, runYamlLaneGitMirrorSyncJob, yamlLanePipelineRunCreateScript } from "./secrets";
import { capture, captureJsonPayload, compactCapture, isGitSha, progressEvent, stringOrNull } from "./utils";
import { runYamlLaneGitopsPublishJob, waitForYamlLaneBuildImage, waitForYamlLaneSourceBootstrap, yamlLaneBuildImageSubmitScript, yamlLaneSourceBootstrapSubmitScript, yamlLaneSourceRestoreScript } from "./yaml-lane";
import { runYamlLaneGitopsPublishJob, runYamlLaneK3sBuildImageJob, waitForYamlLaneBuildImage, waitForYamlLaneSourceBootstrap, yamlLaneBuildImageSubmitScript, yamlLaneK3sSourceStatusScript, yamlLaneSourceBootstrapSubmitScript, yamlLaneSourceRestoreScript } from "./yaml-lane";
export async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spec: AgentRunLaneSpec, configPath: string, waited: boolean): Promise<Record<string, unknown>> {
const result = await triggerCurrentYamlLaneConfirmedSteps(config, spec, configPath, waited);
if (spec.source.statusMode === "k3s-git-mirror") {
return {
...result,
sourceWorkspaceRestore: { ok: true, status: "skipped", statusMode: spec.source.statusMode, reason: "k3s-git-mirror-does-not-use-host-worktree", valuesPrinted: false },
valuesPrinted: false,
};
}
const restore = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceRestoreScript(spec)]);
const restorePayload = captureJsonPayload(restore);
const restoreOk = restore.exitCode === 0 && restorePayload.ok === true;
@@ -53,63 +60,27 @@ export async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spe
}
export async function triggerCurrentYamlLaneConfirmedSteps(config: UniDeskConfig, spec: AgentRunLaneSpec, configPath: string, waited: boolean): Promise<Record<string, unknown>> {
progressEvent("agentrun.yaml-lane.source-bootstrap.progress", {
node: spec.nodeId,
lane: spec.lane,
status: "submitting",
});
const bootstrapSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapSubmitScript(spec)]);
const bootstrapSubmitPayload = captureJsonPayload(bootstrapSubmit);
if (bootstrapSubmit.exitCode !== 0 || bootstrapSubmitPayload.ok === false) {
const source = await resolveTriggerCurrentSource(config, spec, configPath, waited);
if (source.ok !== true) return source;
const sourceCommit = stringOrNull(source.sourceCommit);
const bootstrapPayload = source.sourcePayload;
if (sourceCommit === null || !isGitSha(sourceCommit)) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "source-bootstrap-submit",
degradedReason: "yaml-lane-source-bootstrap-submit-failed",
result: bootstrapSubmitPayload,
capture: compactCapture(bootstrapSubmit, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
valuesPrinted: false,
};
}
const bootstrap = await waitForYamlLaneSourceBootstrap(config, spec, stringOrNull(bootstrapSubmitPayload.jobId));
const bootstrapPayload = bootstrap.payload;
const sourceCommit = stringOrNull(bootstrapPayload.sourceCommit);
if (bootstrap.ok !== true || sourceCommit === null || !isGitSha(sourceCommit)) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "source-bootstrap",
degradedReason: "yaml-lane-source-bootstrap-failed",
phase: "source-resolve",
degradedReason: "yaml-lane-source-commit-invalid",
result: bootstrapPayload,
bootstrapStatus: bootstrap,
valuesPrinted: false,
};
}
const buildSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneBuildImageSubmitScript(spec, sourceCommit)]);
const buildSubmitPayload = captureJsonPayload(buildSubmit);
if (buildSubmit.exitCode !== 0 || buildSubmitPayload.ok === false) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "image-build-submit",
sourceCommit,
degradedReason: "yaml-lane-image-build-submit-failed",
sourceBootstrap: bootstrapPayload,
result: buildSubmitPayload,
capture: compactCapture(buildSubmit, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
valuesPrinted: false,
};
}
const build = await waitForYamlLaneBuildImage(config, spec, sourceCommit, stringOrNull(buildSubmitPayload.jobId));
const buildResult = await buildTriggerCurrentImage(config, spec, sourceCommit, bootstrapPayload, configPath, waited);
if (buildResult.ok !== true) return buildResult;
const build = buildResult.buildStatus;
const buildSubmitPayload = buildResult.buildSubmitPayload;
const buildPayload = build.payload;
const digest = stringOrNull(buildPayload.digest);
const envIdentity = stringOrNull(buildPayload.envIdentity);
@@ -227,3 +198,124 @@ export async function triggerCurrentYamlLaneConfirmedSteps(config: UniDeskConfig
valuesPrinted: false,
};
}
async function resolveTriggerCurrentSource(config: UniDeskConfig, spec: AgentRunLaneSpec, configPath: string, waited: boolean): Promise<Record<string, unknown> & { ok: boolean; sourceCommit?: string | null; sourcePayload?: Record<string, unknown> }> {
if (spec.source.statusMode === "k3s-git-mirror") {
progressEvent("agentrun.yaml-lane.source-status.progress", {
node: spec.nodeId,
lane: spec.lane,
statusMode: spec.source.statusMode,
status: "probing",
});
const sourceStatus = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneK3sSourceStatusScript(spec)]);
const sourcePayload = captureJsonPayload(sourceStatus);
const sourceCommit = stringOrNull(sourcePayload.sourceCommit) ?? stringOrNull(sourcePayload.remoteBranchCommit);
if (sourceStatus.exitCode !== 0 || sourcePayload.ok !== true || sourceCommit === null || !isGitSha(sourceCommit)) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "source-status",
degradedReason: "yaml-lane-k3s-source-status-failed",
result: sourcePayload,
capture: compactCapture(sourceStatus, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
valuesPrinted: false,
};
}
return { ok: true, sourceCommit, sourcePayload, valuesPrinted: false };
}
progressEvent("agentrun.yaml-lane.source-bootstrap.progress", {
node: spec.nodeId,
lane: spec.lane,
status: "submitting",
});
const bootstrapSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapSubmitScript(spec)]);
const bootstrapSubmitPayload = captureJsonPayload(bootstrapSubmit);
if (bootstrapSubmit.exitCode !== 0 || bootstrapSubmitPayload.ok === false) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "source-bootstrap-submit",
degradedReason: "yaml-lane-source-bootstrap-submit-failed",
result: bootstrapSubmitPayload,
capture: compactCapture(bootstrapSubmit, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
valuesPrinted: false,
};
}
const bootstrap = await waitForYamlLaneSourceBootstrap(config, spec, stringOrNull(bootstrapSubmitPayload.jobId));
const sourcePayload = bootstrap.payload;
const sourceCommit = stringOrNull(sourcePayload.sourceCommit);
if (bootstrap.ok !== true || sourceCommit === null || !isGitSha(sourceCommit)) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "source-bootstrap",
degradedReason: "yaml-lane-source-bootstrap-failed",
result: sourcePayload,
bootstrapStatus: bootstrap,
valuesPrinted: false,
};
}
return { ok: true, sourceCommit, sourcePayload, sourceBootstrapSubmit: bootstrapSubmitPayload, valuesPrinted: false };
}
async function buildTriggerCurrentImage(config: UniDeskConfig, spec: AgentRunLaneSpec, sourceCommit: string, sourcePayload: Record<string, unknown>, configPath: string, waited: boolean): Promise<Record<string, unknown> & { ok: boolean; buildStatus?: Record<string, unknown> & { ok: boolean; payload: Record<string, unknown> }; buildSubmitPayload?: Record<string, unknown> }> {
if (spec.source.statusMode === "k3s-git-mirror") {
const build = await runYamlLaneK3sBuildImageJob(config, spec, sourceCommit);
const buildSubmitPayload = {
ok: build.ok !== false,
status: "submitted",
mode: "k3s-buildkit-job",
jobName: stringOrNull(build.jobName) ?? null,
valuesPrinted: false,
};
if (build.ok !== true) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "image-build",
sourceCommit,
degradedReason: "yaml-lane-k3s-image-build-failed",
sourceBootstrap: sourcePayload,
buildSubmit: buildSubmitPayload,
result: build.payload,
buildStatus: build,
valuesPrinted: false,
};
}
return { ok: true, buildStatus: build, buildSubmitPayload, valuesPrinted: false };
}
const buildSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneBuildImageSubmitScript(spec, sourceCommit)]);
const buildSubmitPayload = captureJsonPayload(buildSubmit);
if (buildSubmit.exitCode !== 0 || buildSubmitPayload.ok === false) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "image-build-submit",
sourceCommit,
degradedReason: "yaml-lane-image-build-submit-failed",
sourceBootstrap: sourcePayload,
result: buildSubmitPayload,
capture: compactCapture(buildSubmit, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
valuesPrinted: false,
};
}
const build = await waitForYamlLaneBuildImage(config, spec, sourceCommit, stringOrNull(buildSubmitPayload.jobId));
return { ok: true, buildStatus: build, buildSubmitPayload, valuesPrinted: false };
}
+8 -4
View File
@@ -38,7 +38,7 @@ import { agentRunControlPlaneStatusCommand } from "./public-exposure";
import { applyYamlScript, manifestObjectRef, yamlLaneGitMirrorStatusScript } from "./secrets";
import { compactAgentRunLaneStatusTarget, compactLaneSecretsStatus } from "./trigger";
import { capture, captureJsonPayload, compactCapture, record, stringOrNull, timedStatusStage } from "./utils";
import { yamlLaneRuntimeStatusScript, yamlLaneSourceStatusScript } from "./yaml-lane";
import { yamlLaneK3sSourceStatusScript, yamlLaneRuntimeStatusScript, yamlLaneSourceStatusScript } from "./yaml-lane";
export function parseSecretSyncOptions(args: string[]): SecretSyncOptions {
const base = parseConfirmOptions(args);
@@ -315,7 +315,9 @@ export async function status(config: UniDeskConfig, options: StatusOptions): Pro
export async function statusYamlLane(config: UniDeskConfig, options: StatusOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
const spec = target.spec;
const sourceProbe = await timedStatusStage("source", () => capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)]));
const sourceProbe = await timedStatusStage("source", () => spec.source.statusMode === "k3s-git-mirror"
? capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneK3sSourceStatusScript(spec)])
: capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)]));
const sourcePayload = captureJsonPayload(sourceProbe.value);
const branchTipCommit = stringOrNull(sourcePayload.remoteBranchCommit) ?? stringOrNull(sourcePayload.localHead);
const initialSourceCommit = options.sourceCommit
@@ -362,14 +364,15 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio
: null,
valuesPrinted: false,
};
const sourceWorkspaceRequired = spec.source.statusMode === "host-worktree";
const warnings = [
...(sourceWorktreeDetached ? ["source-worktree-detached"] : []),
...(sourceBranchAdvanced ? ["source-branch-advanced-after-target"] : []),
];
const blockers = [
...(sourcePayload.workspaceExists === true ? [] : ["source-worktree-missing"]),
...(sourceWorkspaceRequired && sourcePayload.workspaceExists !== true ? ["source-worktree-missing"] : []),
...(sourcePayload.remoteBranchExists === true ? [] : ["source-branch-missing"]),
...(sourcePayload.workspaceClean === true || sourcePayload.workspaceExists !== true ? [] : ["source-worktree-dirty"]),
...(sourceWorkspaceRequired && sourcePayload.workspaceClean !== true && sourcePayload.workspaceExists === true ? ["source-worktree-dirty"] : []),
...(mirrorPayload.readReady === true ? [] : ["git-mirror-read-not-ready"]),
...(mirrorPayload.writeReady === true ? [] : ["git-mirror-write-not-ready"]),
...(mirrorPayload.cacheReady === true ? [] : ["git-mirror-cache-not-ready"]),
@@ -422,6 +425,7 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio
? { code: "runtime-blocked", summary: `runtime alignment is blocked: ${runtimeBlockers.join(", ")}`, command: statusFullCommand }
: { code: "inspect-full-status", summary: "alignment is inconclusive; inspect full status details", command: statusFullCommand };
const compactSourceStatus = {
statusMode: spec.source.statusMode,
workspaceExists: sourcePayload.workspaceExists ?? false,
workspaceClean: sourcePayload.workspaceClean ?? null,
branch: sourcePayload.branch ?? null,
+21 -15
View File
@@ -446,21 +446,27 @@ export function refreshYamlLaneScript(spec: AgentRunLaneSpec): string {
`application=${shQuote(spec.gitops.argoApplication)}`,
"kubectl -n \"$namespace\" annotate application \"$application\" argocd.argoproj.io/refresh=hard --overwrite >/tmp/agentrun-refresh-output.txt",
"kubectl -n \"$namespace\" get application \"$application\" -o json >/tmp/agentrun-refresh-app.json",
"NAMESPACE=\"$namespace\" APPLICATION=\"$application\" node <<'NODE'",
"const fs = require('node:fs');",
"let app = {};",
"try { app = JSON.parse(fs.readFileSync('/tmp/agentrun-refresh-app.json', 'utf8')); } catch {}",
"console.log(JSON.stringify({",
" ok: true,",
" namespace: process.env.NAMESPACE,",
" application: process.env.APPLICATION,",
" revision: app.status?.sync?.revision ?? null,",
" syncStatus: app.status?.sync?.status ?? null,",
" healthStatus: app.status?.health?.status ?? null,",
" observedAt: new Date().toISOString(),",
" valuesPrinted: false",
"}));",
"NODE",
"NAMESPACE=\"$namespace\" APPLICATION=\"$application\" python3 - <<'PY'",
"import datetime, json, os",
"try:",
" with open('/tmp/agentrun-refresh-app.json', 'r', encoding='utf-8') as fh:",
" app = json.load(fh)",
"except Exception:",
" app = {}",
"status = app.get('status') or {}",
"sync = status.get('sync') or {}",
"health = status.get('health') or {}",
"print(json.dumps({",
" 'ok': True,",
" 'namespace': os.environ.get('NAMESPACE'),",
" 'application': os.environ.get('APPLICATION'),",
" 'revision': sync.get('revision'),",
" 'syncStatus': sync.get('status'),",
" 'healthStatus': health.get('status'),",
" 'observedAt': datetime.datetime.now(datetime.timezone.utc).isoformat().replace('+00:00', 'Z'),",
" 'valuesPrinted': False,",
"}, ensure_ascii=False))",
"PY",
].join("\n");
}
+182 -95
View File
@@ -343,9 +343,10 @@ export function yamlLanePipelineRunCreateScript(spec: AgentRunLaneSpec, sourceCo
"if [ \"$existing_status\" = False ]; then kubectl -n \"$namespace\" delete pipelinerun \"$pipeline_run\" --ignore-not-found=true --wait=false >/dev/null 2>&1 || true; fi",
"if kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" >/dev/null 2>&1; then created=false; else kubectl create -f \"$tmp\"; created=true; fi",
"status=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o 'jsonpath={.status.conditions[0].status}' 2>/dev/null || true)",
"CREATED=\"$created\" STATUS=\"$status\" PIPELINE_RUN=\"$pipeline_run\" node <<'NODE'",
"console.log(JSON.stringify({ ok: true, pipelineRun: process.env.PIPELINE_RUN, created: process.env.CREATED === 'true', status: process.env.STATUS || null, valuesPrinted: false }));",
"NODE",
"CREATED=\"$created\" STATUS=\"$status\" PIPELINE_RUN=\"$pipeline_run\" python3 - <<'PY'",
"import json, os",
"print(json.dumps({'ok': True, 'pipelineRun': os.environ.get('PIPELINE_RUN'), 'created': os.environ.get('CREATED') == 'true', 'status': os.environ.get('STATUS') or None, 'valuesPrinted': False}, ensure_ascii=False))",
"PY",
].join("\n");
}
@@ -361,9 +362,10 @@ export function createYamlLaneJobScript(namespace: string, jobName: string, mani
"printf '%s' \"$manifest_b64\" | base64 -d > \"$tmp\"",
"kubectl -n \"$namespace\" delete job \"$job\" --ignore-not-found=true --wait=false >/dev/null 2>&1 || true",
"kubectl create -f \"$tmp\"",
"JOB=\"$job\" node <<'NODE'",
"console.log(JSON.stringify({ ok: true, jobName: process.env.JOB, valuesPrinted: false }));",
"NODE",
"JOB=\"$job\" python3 - <<'PY'",
"import json, os",
"print(json.dumps({'ok': True, 'jobName': os.environ.get('JOB'), 'valuesPrinted': False}, ensure_ascii=False))",
"PY",
].join("\n");
}
@@ -375,14 +377,27 @@ export function yamlLaneJobProbeScript(namespace: string, jobName: string): stri
"kubectl -n \"$namespace\" get job \"$job\" -o json > /tmp/agentrun-job.json 2>/dev/null",
"job_exit=$?",
"kubectl -n \"$namespace\" logs \"job/$job\" --tail=120 > /tmp/agentrun-job.log 2>/dev/null",
"JOB_EXIT=\"$job_exit\" JOB=\"$job\" node <<'NODE'",
"const fs = require('node:fs');",
"let job = null; try { job = JSON.parse(fs.readFileSync('/tmp/agentrun-job.json', 'utf8')); } catch {}",
"let log = ''; try { log = fs.readFileSync('/tmp/agentrun-job.log', 'utf8'); } catch {}",
"const succeeded = Number(job?.status?.succeeded || 0) > 0;",
"const failed = Number(job?.status?.failed || 0) > 0;",
"console.log(JSON.stringify({ ok: process.env.JOB_EXIT === '0', jobName: process.env.JOB, succeeded, failed, active: job?.status?.active || 0, logsTail: log.slice(-4000), valuesPrinted: false }));",
"NODE",
"JOB_EXIT=\"$job_exit\" JOB=\"$job\" python3 - <<'PY'",
"import json, os",
"def read_json(path):",
" try:",
" with open(path, 'r', encoding='utf-8') as fh:",
" return json.load(fh)",
" except Exception:",
" return None",
"def read_text(path):",
" try:",
" with open(path, 'r', encoding='utf-8') as fh:",
" return fh.read()",
" except Exception:",
" return ''",
"job = read_json('/tmp/agentrun-job.json') or {}",
"status = job.get('status') or {}",
"log = read_text('/tmp/agentrun-job.log')",
"succeeded = int(status.get('succeeded') or 0) > 0",
"failed = int(status.get('failed') or 0) > 0",
"print(json.dumps({'ok': os.environ.get('JOB_EXIT') == '0', 'jobName': os.environ.get('JOB'), 'succeeded': succeeded, 'failed': failed, 'active': status.get('active') or 0, 'logsTail': log[-4000:], 'valuesPrinted': False}, ensure_ascii=False))",
"PY",
].join("\n");
}
@@ -619,38 +634,53 @@ export function yamlLaneGitMirrorStatusScript(spec: AgentRunLaneSpec): string {
"repo_path=\"/cache/${repository}.git\"",
"rm -f /tmp/agentrun-gitmirror-refs.txt",
"if [ \"$read_exit\" -eq 0 ]; then",
" kubectl -n \"$namespace\" exec deploy/\"$read_deployment\" -- sh -lc 'repo_path=\"$1\"; source_branch=\"$2\"; gitops_branch=\"$3\"; printf \"sourceCommit=\"; git --git-dir=\"$repo_path\" rev-parse \"refs/heads/$source_branch\" 2>/dev/null || true; printf \"gitopsCommit=\"; git --git-dir=\"$repo_path\" rev-parse \"refs/heads/$gitops_branch\" 2>/dev/null || true' sh \"$repo_path\" \"$source_branch\" \"$gitops_branch\" > /tmp/agentrun-gitmirror-refs.txt 2>/dev/null",
" kubectl -n \"$namespace\" exec deploy/\"$read_deployment\" -- sh -lc 'repo_path=\"$1\"; source_branch=\"$2\"; gitops_branch=\"$3\"; source_commit=$(git --git-dir=\"$repo_path\" rev-parse --verify \"refs/heads/$source_branch^{commit}\" 2>/dev/null || true); gitops_commit=$(git --git-dir=\"$repo_path\" rev-parse --verify \"refs/heads/$gitops_branch^{commit}\" 2>/dev/null || true); printf \"sourceCommit=%s\\n\" \"$source_commit\"; printf \"gitopsCommit=%s\\n\" \"$gitops_commit\"' sh \"$repo_path\" \"$source_branch\" \"$gitops_branch\" > /tmp/agentrun-gitmirror-refs.txt 2>/dev/null",
"fi",
"NAMESPACE=\"$namespace\" READ_EXIT=\"$read_exit\" WRITE_EXIT=\"$write_exit\" CACHE_EXIT=\"$cache_exit\" CACHE_MODE=\"$cache_mode\" CACHE_HOST_PATH=\"$cache_host_path\" REPOSITORY=\"$repository\" SOURCE_BRANCH=\"$source_branch\" GITOPS_BRANCH=\"$gitops_branch\" REPOSITORIES_JSON=\"$repositories_json\" node <<'NODE'",
"const fs = require('node:fs');",
"const repositories = JSON.parse(process.env.REPOSITORIES_JSON || '[]');",
"let names = ''; try { names = fs.readFileSync('/tmp/agentrun-gitmirror-names.txt', 'utf8'); } catch {}",
"let refs = ''; try { refs = fs.readFileSync('/tmp/agentrun-gitmirror-refs.txt', 'utf8'); } catch {}",
"const refValue = (key) => refs.split(/\\r?\\n/).find((line) => line.startsWith(`${key}=`))?.slice(key.length + 1).trim() || null;",
"const readReady = process.env.READ_EXIT === '0';",
"const writeReady = process.env.WRITE_EXIT === '0';",
"const cachePvcExists = process.env.CACHE_EXIT === '0';",
"const cacheMode = process.env.CACHE_MODE || 'pvc';",
"const cacheReady = cacheMode === 'hostPath' ? true : cachePvcExists;",
"console.log(JSON.stringify({",
" ok: readReady && writeReady && cacheReady,",
" namespace: process.env.NAMESPACE,",
" readReady,",
" writeReady,",
" cacheMode,",
" cacheReady,",
" cachePvcExists,",
" cacheHostPath: process.env.CACHE_HOST_PATH || null,",
" resources: names.split(/\\r?\\n/).filter(Boolean).slice(0, 40),",
" repository: process.env.REPOSITORY,",
" sourceBranch: process.env.SOURCE_BRANCH,",
" gitopsBranch: process.env.GITOPS_BRANCH,",
" sourceCommit: refValue('sourceCommit'),",
" gitopsCommit: refValue('gitopsCommit'),",
" repositories,",
" valuesPrinted: false",
"}));",
"NODE",
"NAMESPACE=\"$namespace\" READ_EXIT=\"$read_exit\" WRITE_EXIT=\"$write_exit\" CACHE_EXIT=\"$cache_exit\" CACHE_MODE=\"$cache_mode\" CACHE_HOST_PATH=\"$cache_host_path\" REPOSITORY=\"$repository\" SOURCE_BRANCH=\"$source_branch\" GITOPS_BRANCH=\"$gitops_branch\" REPOSITORIES_JSON=\"$repositories_json\" python3 - <<'PY'",
"import json, os, re",
"def read_text(path):",
" try:",
" with open(path, 'r', encoding='utf-8') as fh:",
" return fh.read()",
" except Exception:",
" return ''",
"def ref_value(text, key):",
" prefix = key + '='",
" for line in re.split(r'\\r?\\n', text):",
" if line.startswith(prefix):",
" value = line[len(prefix):].strip()",
" return value or None",
" return None",
"try:",
" repositories = json.loads(os.environ.get('REPOSITORIES_JSON') or '[]')",
"except Exception:",
" repositories = []",
"names = read_text('/tmp/agentrun-gitmirror-names.txt')",
"refs = read_text('/tmp/agentrun-gitmirror-refs.txt')",
"read_ready = os.environ.get('READ_EXIT') == '0'",
"write_ready = os.environ.get('WRITE_EXIT') == '0'",
"cache_pvc_exists = os.environ.get('CACHE_EXIT') == '0'",
"cache_mode = os.environ.get('CACHE_MODE') or 'pvc'",
"cache_ready = True if cache_mode == 'hostPath' else cache_pvc_exists",
"print(json.dumps({",
" 'ok': read_ready and write_ready and cache_ready,",
" 'namespace': os.environ.get('NAMESPACE'),",
" 'readReady': read_ready,",
" 'writeReady': write_ready,",
" 'cacheMode': cache_mode,",
" 'cacheReady': cache_ready,",
" 'cachePvcExists': cache_pvc_exists,",
" 'cacheHostPath': os.environ.get('CACHE_HOST_PATH') or None,",
" 'resources': [line for line in re.split(r'\\r?\\n', names) if line][:40],",
" 'repository': os.environ.get('REPOSITORY'),",
" 'sourceBranch': os.environ.get('SOURCE_BRANCH'),",
" 'gitopsBranch': os.environ.get('GITOPS_BRANCH'),",
" 'sourceCommit': ref_value(refs, 'sourceCommit'),",
" 'gitopsCommit': ref_value(refs, 'gitopsCommit'),",
" 'repositories': repositories,",
" 'valuesPrinted': False",
"}, ensure_ascii=False))",
"PY",
].join("\n");
}
@@ -660,6 +690,7 @@ export type LaneSecretSource = {
sourceMode: "env" | "file";
sourceKey: string | null;
targetRef: { namespace: string; name: string; key: string };
transform?: "local-postgres-database" | "local-postgres-user" | "local-postgres-database-url";
};
export function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecretSource): { redactedPath: string; value: string; valueBytes: number; fingerprint: string } {
@@ -680,6 +711,7 @@ export function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecret
value = envValue;
}
if (value.length === 0) throw new Error(`secret source ${sourceRef} is empty`);
value = transformSecretSourceValue(spec, source, value);
return {
redactedPath: sourceRef.startsWith("/") ? redactAbsoluteSecretPath(sourceRef) : `.state/secrets/${sourceRef}`,
value,
@@ -688,6 +720,20 @@ export function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecret
};
}
function transformSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecretSource, rawValue: string): string {
if (source.transform === undefined) return rawValue;
const localPostgres = spec.deployment.localPostgres;
if (!localPostgres.enabled || localPostgres.serviceName === null || localPostgres.database === null || localPostgres.user === null || localPostgres.port === null) {
throw new Error(`secret source ${source.id} requires enabled localPostgres with database/user/port`);
}
if (source.transform === "local-postgres-database") return localPostgres.database;
if (source.transform === "local-postgres-user") return localPostgres.user;
const user = encodeURIComponent(localPostgres.user);
const password = encodeURIComponent(rawValue);
const database = encodeURIComponent(localPostgres.database);
return `postgresql://${user}:${password}@${localPostgres.serviceName}:${localPostgres.port}/${database}`;
}
export function redactAbsoluteSecretPath(sourceRef: string): string {
const parts = sourceRef.split("/").filter(Boolean);
return parts.length === 0 ? "/" : `/${parts.slice(0, -1).join("/")}/<redacted>`;
@@ -732,6 +778,45 @@ export function collectLaneSecretSources(spec: AgentRunLaneSpec): LaneSecretSour
targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: spec.database.secretRef.key },
});
}
const localPostgres = spec.deployment.localPostgres;
if (spec.database.mode === "local-postgres" && localPostgres.enabled) {
if (localPostgres.passwordSourceRef === null || localPostgres.passwordSourceKey === null) {
throw new Error(`config/agentrun.yaml controlPlane.lanes.${spec.lane}.deployment.localPostgres must declare passwordSourceRef/passwordSourceKey`);
}
result.push(
{
id: "local-postgres-password",
sourceRef: localPostgres.passwordSourceRef,
sourceMode: "env",
sourceKey: localPostgres.passwordSourceKey,
targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: "POSTGRES_PASSWORD" },
},
{
id: "local-postgres-user",
sourceRef: localPostgres.passwordSourceRef,
sourceMode: "env",
sourceKey: localPostgres.passwordSourceKey,
targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: "POSTGRES_USER" },
transform: "local-postgres-user",
},
{
id: "local-postgres-database",
sourceRef: localPostgres.passwordSourceRef,
sourceMode: "env",
sourceKey: localPostgres.passwordSourceKey,
targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: "POSTGRES_DB" },
transform: "local-postgres-database",
},
{
id: "database",
sourceRef: localPostgres.passwordSourceRef,
sourceMode: "env",
sourceKey: localPostgres.passwordSourceKey,
targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: spec.database.secretRef.key },
transform: "local-postgres-database-url",
},
);
}
for (const secret of spec.secrets) {
result.push({
id: secret.id,
@@ -762,47 +847,44 @@ export function secretSyncScript(_spec: AgentRunLaneSpec, values: Array<{ target
"trap 'rm -rf \"$tmp_dir\"' EXIT",
"payload_json=\"$tmp_dir/payload.json\"",
"printf '%s' \"$payload_b64\" | base64 -d > \"$payload_json\"",
"PAYLOAD_JSON=\"$payload_json\" TMP_DIR=\"$tmp_dir\" node <<'NODE'",
"const fs = require('node:fs');",
"const cp = require('node:child_process');",
"const crypto = require('node:crypto');",
"const items = JSON.parse(fs.readFileSync(process.env.PAYLOAD_JSON, 'utf8'));",
"const results = [];",
"const groups = new Map();",
"function run(argv, input) {",
" const out = cp.spawnSync(argv[0], argv.slice(1), { input, encoding: 'utf8' });",
" if (out.status !== 0) throw new Error(`${argv.join(' ')} failed: ${out.stderr || out.stdout}`);",
" return out;",
"}",
"function tryRun(argv) {",
" return cp.spawnSync(argv[0], argv.slice(1), { encoding: 'utf8' });",
"}",
"for (let index = 0; index < items.length; index += 1) {",
" const item = items[index];",
" const ref = item.targetRef;",
" const groupKey = `${ref.namespace}\\u0000${ref.name}`;",
" if (!groups.has(groupKey)) groups.set(groupKey, { namespace: ref.namespace, name: ref.name, items: [] });",
" groups.get(groupKey).items.push({ key: ref.key, valueBase64: item.valueBase64 });",
"}",
"for (const group of groups.values()) {",
" const ns = run(['kubectl', 'create', 'namespace', group.namespace, '--dry-run=client', '-o', 'yaml']).stdout;",
" run(['kubectl', 'apply', '--server-side', '--field-manager=unidesk-agentrun-secret-sync', '-f', '-'], ns);",
" const existing = tryRun(['kubectl', '-n', group.namespace, 'get', 'secret', group.name]);",
" if (existing.status !== 0) run(['kubectl', '-n', group.namespace, 'create', 'secret', 'generic', group.name]);",
" const patch = { data: {} };",
" for (const entry of group.items) patch.data[entry.key] = entry.valueBase64;",
" const patchFile = `${process.env.TMP_DIR}/${group.namespace}-${group.name}-patch.json`;",
" fs.writeFileSync(patchFile, JSON.stringify(patch));",
" run(['kubectl', '-n', group.namespace, 'patch', 'secret', group.name, '--type=merge', `--patch-file=${patchFile}`]);",
" const fetched = JSON.parse(run(['kubectl', '-n', group.namespace, 'get', 'secret', group.name, '-o', 'json']).stdout);",
" for (const entry of group.items) {",
" const raw = fetched.data?.[entry.key] || '';",
" const decoded = raw ? Buffer.from(raw, 'base64') : Buffer.alloc(0);",
" results.push({ namespace: group.namespace, secret: group.name, key: entry.key, ok: raw.length > 0, valueBytes: decoded.length, fingerprint: raw ? 'sha256:' + crypto.createHash('sha256').update(decoded).digest('hex') : null, valuesPrinted: false });",
" }",
"}",
"console.log(JSON.stringify({ ok: results.every((item) => item.ok), secretCount: results.length, items: results, valuesPrinted: false }));",
"NODE",
"PAYLOAD_JSON=\"$payload_json\" TMP_DIR=\"$tmp_dir\" python3 - <<'PY'",
"import base64, hashlib, json, os, subprocess",
"def run(argv, input_text=None):",
" out = subprocess.run(argv, input=input_text, text=True, capture_output=True)",
" if out.returncode != 0:",
" raise RuntimeError('%s failed: %s' % (' '.join(argv), (out.stderr or out.stdout).strip()))",
" return out",
"def try_run(argv):",
" return subprocess.run(argv, text=True, capture_output=True)",
"with open(os.environ['PAYLOAD_JSON'], 'r', encoding='utf-8') as fh:",
" items = json.load(fh)",
"groups = {}",
"for item in items:",
" ref = item.get('targetRef') or {}",
" key = (ref.get('namespace'), ref.get('name'))",
" groups.setdefault(key, {'namespace': ref.get('namespace'), 'name': ref.get('name'), 'items': []})['items'].append({'key': ref.get('key'), 'valueBase64': item.get('valueBase64') or ''})",
"results = []",
"for group in groups.values():",
" namespace = group['namespace']",
" name = group['name']",
" ns_yaml = run(['kubectl', 'create', 'namespace', namespace, '--dry-run=client', '-o', 'yaml']).stdout",
" run(['kubectl', 'apply', '--server-side', '--field-manager=unidesk-agentrun-secret-sync', '-f', '-'], ns_yaml)",
" existing = try_run(['kubectl', '-n', namespace, 'get', 'secret', name])",
" if existing.returncode != 0:",
" run(['kubectl', '-n', namespace, 'create', 'secret', 'generic', name])",
" patch = {'data': {entry['key']: entry['valueBase64'] for entry in group['items']}}",
" patch_file = os.path.join(os.environ['TMP_DIR'], '%s-%s-patch.json' % (namespace, name))",
" with open(patch_file, 'w', encoding='utf-8') as fh:",
" json.dump(patch, fh)",
" run(['kubectl', '-n', namespace, 'patch', 'secret', name, '--type=merge', '--patch-file=%s' % patch_file])",
" fetched = json.loads(run(['kubectl', '-n', namespace, 'get', 'secret', name, '-o', 'json']).stdout)",
" data = fetched.get('data') or {}",
" for entry in group['items']:",
" raw = data.get(entry['key']) or ''",
" decoded = base64.b64decode(raw.encode('utf-8')) if raw else b''",
" results.append({'namespace': namespace, 'secret': name, 'key': entry['key'], 'ok': bool(raw), 'valueBytes': len(decoded), 'fingerprint': ('sha256:' + hashlib.sha256(decoded).hexdigest()) if raw else None, 'valuesPrinted': False})",
"print(json.dumps({'ok': all(item['ok'] for item in results), 'secretCount': len(results), 'items': results, 'valuesPrinted': False}, ensure_ascii=False))",
"PY",
].join("\n");
}
@@ -869,15 +951,20 @@ export function applyYamlScript(yaml: string, fieldManager: string, dryRun: bool
"kubectl apply $args -f \"$manifest\" > \"$tmp_dir/apply.out\" 2> \"$tmp_dir/apply.err\"",
"apply_exit=$?",
"set -e",
"APPLY_EXIT=\"$apply_exit\" APPLY_OUT=\"$tmp_dir/apply.out\" APPLY_ERR=\"$tmp_dir/apply.err\" MANIFEST=\"$manifest\" node <<'NODE'",
"const fs = require('node:fs');",
"const crypto = require('node:crypto');",
"const out = fs.readFileSync(process.env.APPLY_OUT, 'utf8');",
"const err = fs.readFileSync(process.env.APPLY_ERR, 'utf8');",
"const manifest = fs.readFileSync(process.env.MANIFEST, 'utf8');",
"const resources = out.split(/\\r?\\n/).map((line) => line.trim()).filter(Boolean).slice(0, 80);",
"console.log(JSON.stringify({ ok: process.env.APPLY_EXIT === '0', exitCode: Number(process.env.APPLY_EXIT), resourceCount: resources.length, resources, manifestBytes: Buffer.byteLength(manifest, 'utf8'), manifestDigest: 'sha256:' + crypto.createHash('sha256').update(manifest).digest('hex'), stderrTail: err.slice(-3000), valuesPrinted: false }));",
"NODE",
"APPLY_EXIT=\"$apply_exit\" APPLY_OUT=\"$tmp_dir/apply.out\" APPLY_ERR=\"$tmp_dir/apply.err\" MANIFEST=\"$manifest\" python3 - <<'PY'",
"import hashlib, json, os, re",
"def read_bytes(path):",
" try:",
" with open(path, 'rb') as fh:",
" return fh.read()",
" except Exception:",
" return b''",
"out = read_bytes(os.environ['APPLY_OUT']).decode('utf-8', errors='replace')",
"err = read_bytes(os.environ['APPLY_ERR']).decode('utf-8', errors='replace')",
"manifest = read_bytes(os.environ['MANIFEST'])",
"resources = [line.strip() for line in re.split(r'\\r?\\n', out) if line.strip()][:80]",
"print(json.dumps({'ok': os.environ.get('APPLY_EXIT') == '0', 'exitCode': int(os.environ.get('APPLY_EXIT') or '1'), 'resourceCount': len(resources), 'resources': resources, 'manifestBytes': len(manifest), 'manifestDigest': 'sha256:' + hashlib.sha256(manifest).hexdigest(), 'stderrTail': err[-3000:], 'valuesPrinted': False}, ensure_ascii=False))",
"PY",
"exit \"$apply_exit\"",
].join("\n");
}
+363 -67
View File
@@ -354,26 +354,78 @@ export function yamlLaneSourceStatusScript(spec: AgentRunLaneSpec): string {
" actual_workspace=$(pwd)",
"fi",
"export expected_workspace source_branch workspace_exists workspace_clean local_head branch remote_url remote_branch_exists remote_branch_commit status_short actual_workspace",
"node <<'NODE'",
"function nullable(value) { return value && value !== 'null' ? value : null; }",
"function booleanValue(value) { if (value === 'true') return true; if (value === 'false') return false; return null; }",
"const env = process.env;",
"console.log(JSON.stringify({",
" ok: env.workspace_exists === 'true',",
" expectedWorkspace: env.expected_workspace,",
" actualWorkspace: env.actual_workspace || null,",
" workspaceExists: env.workspace_exists === 'true',",
" workspaceClean: booleanValue(env.workspace_clean),",
" branch: nullable(env.branch),",
" remoteUrl: nullable(env.remote_url),",
" localHead: nullable(env.local_head),",
" remoteBranch: env.source_branch,",
" remoteBranchExists: env.remote_branch_exists === 'true',",
" remoteBranchCommit: nullable(env.remote_branch_commit),",
" statusShort: nullable(env.status_short),",
" valuesPrinted: false",
"}));",
"NODE",
"python3 - <<'PY'",
"import json, os",
"def nullable(value):",
" return value if value and value != 'null' else None",
"def boolean_value(value):",
" if value == 'true': return True",
" if value == 'false': return False",
" return None",
"env = os.environ",
"print(json.dumps({",
" 'ok': env.get('workspace_exists') == 'true',",
" 'statusMode': 'host-worktree',",
" 'expectedWorkspace': env.get('expected_workspace'),",
" 'actualWorkspace': env.get('actual_workspace') or None,",
" 'workspaceExists': env.get('workspace_exists') == 'true',",
" 'workspaceClean': boolean_value(env.get('workspace_clean')),",
" 'branch': nullable(env.get('branch')),",
" 'remoteUrl': nullable(env.get('remote_url')),",
" 'localHead': nullable(env.get('local_head')),",
" 'remoteBranch': env.get('source_branch'),",
" 'remoteBranchExists': env.get('remote_branch_exists') == 'true',",
" 'remoteBranchCommit': nullable(env.get('remote_branch_commit')),",
" 'statusShort': nullable(env.get('status_short')),",
" 'valuesPrinted': False,",
"}, ensure_ascii=False))",
"PY",
].join("\n");
}
export function yamlLaneK3sSourceStatusScript(spec: AgentRunLaneSpec): string {
return [
"set +e",
`namespace=${shQuote(spec.gitMirror.namespace)}`,
`read_deployment=${shQuote(spec.gitMirror.readDeployment)}`,
`repository=${shQuote(spec.source.repository)}`,
`source_branch=${shQuote(spec.source.branch)}`,
"repo_path=\"/cache/${repository}.git\"",
"kubectl -n \"$namespace\" get deploy \"$read_deployment\" >/tmp/agentrun-source-read-deploy.txt 2>/dev/null",
"read_exit=$?",
"source_commit=''",
"if [ \"$read_exit\" -eq 0 ]; then",
" source_commit=$(kubectl -n \"$namespace\" exec deploy/\"$read_deployment\" -- sh -lc 'repo_path=\"$1\"; source_branch=\"$2\"; git --git-dir=\"$repo_path\" rev-parse --verify \"refs/heads/$source_branch^{commit}\" 2>/dev/null || true' sh \"$repo_path\" \"$source_branch\" 2>/dev/null | tail -n 1 | tr -d '\\r')",
"fi",
"export namespace read_deployment repository source_branch read_exit source_commit",
"python3 - <<'PY'",
"import json, os",
"source_commit = os.environ.get('source_commit') or None",
"read_ready = os.environ.get('read_exit') == '0'",
"print(json.dumps({",
" 'ok': read_ready and source_commit is not None,",
" 'statusMode': 'k3s-git-mirror',",
" 'expectedWorkspace': None,",
" 'actualWorkspace': None,",
" 'workspaceExists': None,",
" 'workspaceClean': None,",
" 'branch': os.environ.get('source_branch'),",
" 'remoteUrl': None,",
" 'localHead': source_commit,",
" 'remoteBranch': os.environ.get('source_branch'),",
" 'remoteBranchExists': source_commit is not None,",
" 'remoteBranchCommit': source_commit,",
" 'sourceCommit': source_commit,",
" 'gitMirror': {",
" 'namespace': os.environ.get('namespace'),",
" 'readDeployment': os.environ.get('read_deployment'),",
" 'readReady': read_ready,",
" 'repository': os.environ.get('repository'),",
" },",
" 'statusShort': None,",
" 'valuesPrinted': False,",
"}, ensure_ascii=False))",
"PY",
].join("\n");
}
@@ -412,54 +464,70 @@ export function yamlLaneRuntimeStatusScript(spec: AgentRunLaneSpec, pipelineRun:
"manager_svc_exit=$?",
"kubectl -n \"$runtime_namespace\" get secret \"$database_secret\" -o json > \"$tmp_dir/db-secret.json\" 2>/dev/null",
"db_secret_exit=$?",
"SECRET_REFS_JSON=\"$secrets_json\" NODE_TMP=\"$tmp_dir\" node <<'NODE' > \"$tmp_dir/secrets.json\"",
"const fs = require('node:fs');",
"const cp = require('node:child_process');",
"const refs = JSON.parse(process.env.SECRET_REFS_JSON || '[]');",
"const result = [];",
"for (const ref of refs) {",
" const out = cp.spawnSync('kubectl', ['-n', ref.namespace, 'get', 'secret', ref.name, '-o', 'json'], { encoding: 'utf8' });",
" let keyPresent = false;",
" if (out.status === 0) {",
" try { keyPresent = Object.prototype.hasOwnProperty.call((JSON.parse(out.stdout).data || {}), ref.key); } catch {}",
" }",
" result.push({ namespace: ref.namespace, name: ref.name, key: ref.key, present: out.status === 0, keyPresent, valuesPrinted: false });",
"}",
"console.log(JSON.stringify({ ready: result.every((item) => item.present && item.keyPresent), count: result.length, items: result, valuesPrinted: false }));",
"NODE",
"SECRET_REFS_JSON=\"$secrets_json\" python3 - <<'PY' > \"$tmp_dir/secrets.json\"",
"import json, os, subprocess",
"refs = json.loads(os.environ.get('SECRET_REFS_JSON') or '[]')",
"result = []",
"for ref in refs:",
" out = subprocess.run(['kubectl', '-n', ref.get('namespace', ''), 'get', 'secret', ref.get('name', ''), '-o', 'json'], text=True, capture_output=True)",
" key_present = False",
" if out.returncode == 0:",
" try:",
" key_present = ref.get('key') in (json.loads(out.stdout).get('data') or {})",
" except Exception:",
" key_present = False",
" result.append({'namespace': ref.get('namespace'), 'name': ref.get('name'), 'key': ref.get('key'), 'present': out.returncode == 0, 'keyPresent': key_present, 'valuesPrinted': False})",
"print(json.dumps({'ready': all(item['present'] and item['keyPresent'] for item in result), 'count': len(result), 'items': result, 'valuesPrinted': False}, ensure_ascii=False))",
"PY",
"kubectl -n \"$runtime_namespace\" get deploy,sts,svc,secret -o name > \"$tmp_dir/runtime-names.txt\" 2>/dev/null",
"NODE_TMP=\"$tmp_dir\" RUNTIME_NS_EXIT=\"$runtime_ns_exit\" CI_NS_EXIT=\"$ci_ns_exit\" PIPELINE_EXIT=\"$pipeline_exit\" SERVICE_ACCOUNT_EXIT=\"$sa_exit\" PIPELINERUN_EXIT=\"$pr_exit\" ARGO_EXIT=\"$argo_exit\" MANAGER_DEPLOY_EXIT=\"$manager_deploy_exit\" MANAGER_SVC_EXIT=\"$manager_svc_exit\" DB_SECRET_EXIT=\"$db_secret_exit\" node <<'NODE'",
"const fs = require('node:fs');",
"const path = require('node:path');",
"const dir = process.env.NODE_TMP;",
"function readJson(name) { try { return JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')); } catch { return null; } }",
"function exists(exitName) { return process.env[exitName] === '0'; }",
"function condition(obj) { return obj?.status?.conditions?.[0] || null; }",
"function envValue(deploy, name) { return deploy?.spec?.template?.spec?.containers?.[0]?.env?.find((entry) => entry.name === name)?.value || null; }",
"function pipelineRunParam(obj, name) { return (obj?.spec?.params || []).find((entry) => entry.name === name)?.value || null; }",
"const pipelineRun = readJson('pipelinerun.json');",
"const argo = readJson('argo.json');",
"const managerDeploy = readJson('manager-deploy.json');",
"const managerSvc = readJson('manager-svc.json');",
"const dbSecret = readJson('db-secret.json');",
"const secrets = readJson('secrets.json') || { ready: true, count: 0, items: [], valuesPrinted: false };",
"let names = ''; try { names = fs.readFileSync(path.join(dir, 'runtime-names.txt'), 'utf8'); } catch {}",
"const c = condition(pipelineRun);",
"console.log(JSON.stringify({",
" ok: exists('RUNTIME_NS_EXIT') && exists('CI_NS_EXIT'),",
" runtimeNamespaceExists: exists('RUNTIME_NS_EXIT'),",
" ciNamespaceExists: exists('CI_NS_EXIT'),",
" serviceAccountExists: exists('SERVICE_ACCOUNT_EXIT'),",
" pipeline: { exists: exists('PIPELINE_EXIT'), name: process.env.pipeline_name },",
" pipelineRun: { exists: exists('PIPELINERUN_EXIT'), name: process.env.pipeline_run || null, status: c?.status || null, reason: c?.reason || null, sourceCommit: pipelineRunParam(pipelineRun, 'revision'), startTime: pipelineRun?.status?.startTime || null, completionTime: pipelineRun?.status?.completionTime || null },",
" argo: { exists: exists('ARGO_EXIT'), namespace: process.env.argo_namespace, application: process.env.argo_application, revision: argo?.status?.sync?.revision || null, syncStatus: argo?.status?.sync?.status || null, healthStatus: argo?.status?.health?.status || null },",
" manager: { deploymentExists: exists('MANAGER_DEPLOY_EXIT'), serviceExists: exists('MANAGER_SVC_EXIT'), deployment: process.env.manager_deployment, service: process.env.manager_service, image: managerDeploy?.spec?.template?.spec?.containers?.[0]?.image || null, sourceCommit: envValue(managerDeploy, 'AGENTRUN_SOURCE_COMMIT'), servicePorts: Array.isArray(managerSvc?.spec?.ports) ? managerSvc.spec.ports.map((port) => ({ name: port.name || null, port: port.port || null, targetPort: port.targetPort || null })) : [] },",
" database: { secretPresent: exists('DB_SECRET_EXIT'), secretName: process.env.database_secret, key: process.env.database_key, keyPresent: Boolean(dbSecret?.data && Object.prototype.hasOwnProperty.call(dbSecret.data, process.env.database_key || '')) , valuesPrinted: false },",
" secrets,",
" localPostgres: { absent: !/postgres/i.test(names), matchingObjects: names.split(/\\r?\\n/).filter((line) => /postgres/i.test(line)).slice(0, 20) },",
" valuesPrinted: false",
"}));",
"NODE",
"NODE_TMP=\"$tmp_dir\" RUNTIME_NS_EXIT=\"$runtime_ns_exit\" CI_NS_EXIT=\"$ci_ns_exit\" PIPELINE_EXIT=\"$pipeline_exit\" SERVICE_ACCOUNT_EXIT=\"$sa_exit\" PIPELINERUN_EXIT=\"$pr_exit\" ARGO_EXIT=\"$argo_exit\" MANAGER_DEPLOY_EXIT=\"$manager_deploy_exit\" MANAGER_SVC_EXIT=\"$manager_svc_exit\" DB_SECRET_EXIT=\"$db_secret_exit\" python3 - <<'PY'",
"import json, os, pathlib, re",
"base = pathlib.Path(os.environ['NODE_TMP'])",
"def read_json(name):",
" try: return json.loads((base / name).read_text())",
" except Exception: return None",
"def exists(name): return os.environ.get(name) == '0'",
"def condition(obj):",
" conds = (((obj or {}).get('status') or {}).get('conditions') or [])",
" return conds[0] if conds else {}",
"def env_value(deploy, name):",
" containers = (((((deploy or {}).get('spec') or {}).get('template') or {}).get('spec') or {}).get('containers') or [])",
" envs = (containers[0].get('env') or []) if containers else []",
" for entry in envs:",
" if entry.get('name') == name: return entry.get('value')",
" return None",
"def pipeline_run_param(obj, name):",
" for entry in (((obj or {}).get('spec') or {}).get('params') or []):",
" if entry.get('name') == name: return entry.get('value')",
" return None",
"pipeline_run = read_json('pipelinerun.json')",
"argo = read_json('argo.json')",
"manager_deploy = read_json('manager-deploy.json')",
"manager_svc = read_json('manager-svc.json')",
"db_secret = read_json('db-secret.json')",
"secrets = read_json('secrets.json') or {'ready': True, 'count': 0, 'items': [], 'valuesPrinted': False}",
"try: names = (base / 'runtime-names.txt').read_text()",
"except Exception: names = ''",
"c = condition(pipeline_run)",
"ports = []",
"for port in ((((manager_svc or {}).get('spec') or {}).get('ports')) or []):",
" ports.append({'name': port.get('name'), 'port': port.get('port'), 'targetPort': port.get('targetPort')})",
"matching_postgres = [line for line in re.split(r'\\r?\\n', names) if re.search('postgres', line, re.I)][:20]",
"print(json.dumps({",
" 'ok': exists('RUNTIME_NS_EXIT') and exists('CI_NS_EXIT'),",
" 'runtimeNamespaceExists': exists('RUNTIME_NS_EXIT'),",
" 'ciNamespaceExists': exists('CI_NS_EXIT'),",
" 'serviceAccountExists': exists('SERVICE_ACCOUNT_EXIT'),",
" 'pipeline': {'exists': exists('PIPELINE_EXIT'), 'name': os.environ.get('pipeline_name')},",
" 'pipelineRun': {'exists': exists('PIPELINERUN_EXIT'), 'name': os.environ.get('pipeline_run') or None, 'status': c.get('status'), 'reason': c.get('reason'), 'sourceCommit': pipeline_run_param(pipeline_run, 'revision'), 'startTime': ((pipeline_run or {}).get('status') or {}).get('startTime'), 'completionTime': ((pipeline_run or {}).get('status') or {}).get('completionTime')},",
" 'argo': {'exists': exists('ARGO_EXIT'), 'namespace': os.environ.get('argo_namespace'), 'application': os.environ.get('argo_application'), 'revision': (((argo or {}).get('status') or {}).get('sync') or {}).get('revision'), 'syncStatus': (((argo or {}).get('status') or {}).get('sync') or {}).get('status'), 'healthStatus': (((argo or {}).get('status') or {}).get('health') or {}).get('status')},",
" 'manager': {'deploymentExists': exists('MANAGER_DEPLOY_EXIT'), 'serviceExists': exists('MANAGER_SVC_EXIT'), 'deployment': os.environ.get('manager_deployment'), 'service': os.environ.get('manager_service'), 'image': ((((((manager_deploy or {}).get('spec') or {}).get('template') or {}).get('spec') or {}).get('containers') or [{}])[0]).get('image'), 'sourceCommit': env_value(manager_deploy, 'AGENTRUN_SOURCE_COMMIT'), 'servicePorts': ports},",
" 'database': {'secretPresent': exists('DB_SECRET_EXIT'), 'secretName': os.environ.get('database_secret'), 'key': os.environ.get('database_key'), 'keyPresent': os.environ.get('database_key') in (((db_secret or {}).get('data') or {})), 'valuesPrinted': False},",
" 'secrets': secrets,",
" 'localPostgres': {'absent': len(matching_postgres) == 0, 'matchingObjects': matching_postgres},",
" 'valuesPrinted': False,",
"}, ensure_ascii=False))",
"PY",
].join("\n");
}
@@ -930,6 +998,234 @@ export function yamlLaneBuildImageStatusScript(spec: AgentRunLaneSpec, jobId: st
].join("\n");
}
export function yamlLaneK3sBuildImageJobManifest(spec: AgentRunLaneSpec, sourceCommit: string, jobName: string): Record<string, unknown> {
const build = spec.deployment.manager.imageBuild;
if (spec.ci.buildkitImage === null) throw new Error(`config/agentrun.yaml controlPlane.lanes.${spec.lane}.ci.buildkitImage is required when source.statusMode=k3s-git-mirror`);
const imageRepository = `${spec.ci.registryPrefix}/${build.repository}`;
const envIdentity = `source-${sourceCommit.slice(0, 12)}`;
const image = `${imageRepository}:${envIdentity}`;
const proxyEnv = yamlLaneK3sBuildProxyEnv(spec);
return {
apiVersion: "batch/v1",
kind: "Job",
metadata: {
name: jobName,
namespace: spec.ci.namespace,
labels: {
"app.kubernetes.io/name": "agentrun-manager-image-build",
"app.kubernetes.io/part-of": "agentrun",
"agentrun.pikastech.local/lane": spec.version,
"agentrun.pikastech.local/node": spec.nodeId,
"agentrun.pikastech.local/source-commit": sourceCommit,
},
},
spec: {
backoffLimit: 0,
activeDeadlineSeconds: Math.max(60, build.timeoutSeconds),
ttlSecondsAfterFinished: 3600,
template: {
metadata: {
labels: {
"app.kubernetes.io/name": "agentrun-manager-image-build",
"app.kubernetes.io/part-of": "agentrun",
"agentrun.pikastech.local/lane": spec.version,
"agentrun.pikastech.local/node": spec.nodeId,
"agentrun.pikastech.local/source-commit": sourceCommit,
},
},
spec: {
restartPolicy: "Never",
serviceAccountName: spec.ci.serviceAccountName,
hostNetwork: true,
dnsPolicy: "ClusterFirstWithHostNet",
securityContext: { fsGroup: 1000 },
volumes: [
{ name: "workspace", emptyDir: { sizeLimit: "8Gi" } },
{ name: "buildkit-state", emptyDir: { sizeLimit: "8Gi" } },
{ name: "tmp", emptyDir: {} },
],
initContainers: [{
name: "source",
image: spec.ci.toolsImage,
imagePullPolicy: "IfNotPresent",
env: proxyEnv,
command: ["/bin/sh", "-ec", yamlLaneK3sBuildSourceShell(spec, sourceCommit)],
volumeMounts: [{ name: "workspace", mountPath: "/workspace" }],
}],
containers: [{
name: "buildkit",
image: spec.ci.buildkitImage,
imagePullPolicy: "IfNotPresent",
env: [
...proxyEnv,
{ name: "BUILDKITD_FLAGS", value: "--oci-worker-no-process-sandbox --oci-worker-net=host --allow-insecure-entitlement network.host" },
],
command: ["/bin/sh", "-ec", yamlLaneK3sBuildImageShell(spec, sourceCommit, image, envIdentity)],
securityContext: { privileged: true, runAsUser: 1000, runAsGroup: 1000 },
volumeMounts: [
{ name: "workspace", mountPath: "/workspace" },
{ name: "buildkit-state", mountPath: "/home/user/.local/share/buildkit" },
{ name: "tmp", mountPath: "/tmp" },
],
}],
},
},
},
};
}
export async function runYamlLaneK3sBuildImageJob(config: UniDeskConfig, spec: AgentRunLaneSpec, sourceCommit: string): Promise<Record<string, unknown> & { ok: boolean; payload: Record<string, unknown> }> {
const jobName = `agentrun-build-${spec.nodeId.toLowerCase()}-${spec.lane}-${sourceCommit.slice(0, 12)}`.slice(0, 63);
const manifest = yamlLaneK3sBuildImageJobManifest(spec, sourceCommit, jobName);
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.ci.namespace, jobName, manifest)]);
const createPayload = captureJsonPayload(created);
if (created.exitCode !== 0 || createPayload.ok === false) {
return {
ok: false,
payload: { ok: false, status: "create-failed", jobName, degradedReason: "k3s-buildkit-job-create-failed", valuesPrinted: false },
create: createPayload,
capture: compactCapture(created, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
valuesPrinted: false,
};
}
const startedAt = Date.now();
const timeoutMs = Math.max(60, spec.deployment.manager.imageBuild.timeoutSeconds) * 1000;
const pollMs = Math.max(1, spec.deployment.manager.imageBuild.pollSeconds) * 1000;
let polls = 0;
let lastPayload: Record<string, unknown> = {};
let lastProbe: SshCaptureResult | null = null;
while (Date.now() - startedAt < timeoutMs) {
polls += 1;
lastProbe = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneJobProbeScript(spec.ci.namespace, jobName)]);
const probePayload = captureJsonPayload(lastProbe);
const buildPayload = yamlLaneGitopsPublishPayloadFromProbe(probePayload);
if (Object.keys(buildPayload).length > 0) lastPayload = buildPayload;
progressEvent("agentrun.yaml-lane.k3s-image-build.progress", {
node: spec.nodeId,
lane: spec.lane,
sourceCommit,
jobName,
polls,
status: stringOrNull(buildPayload.status) ?? (probePayload.succeeded === true ? "succeeded" : probePayload.failed === true ? "failed" : "running"),
digest: stringOrNull(buildPayload.digest),
elapsedMs: Date.now() - startedAt,
});
if (probePayload.succeeded === true) {
if (buildPayload.ok === true && stringOrNull(buildPayload.digest) !== null && stringOrNull(buildPayload.envIdentity) !== null) {
return { ok: true, payload: buildPayload, jobName, create: createPayload, polls, elapsedMs: Date.now() - startedAt, probe: compactCapture(lastProbe), valuesPrinted: false };
}
return {
ok: false,
payload: { ...buildPayload, ok: false, status: stringOrNull(buildPayload.status) ?? "succeeded-without-result", degradedReason: "k3s-buildkit-result-missing", jobName, valuesPrinted: false },
jobName,
create: createPayload,
polls,
elapsedMs: Date.now() - startedAt,
probe: compactCapture(lastProbe, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
valuesPrinted: false,
};
}
if (probePayload.failed === true) {
const payload = Object.keys(buildPayload).length > 0 ? buildPayload : { ok: false, status: "failed", degradedReason: "k3s-buildkit-job-failed", jobName, valuesPrinted: false };
return {
ok: false,
payload,
jobName,
create: createPayload,
polls,
elapsedMs: Date.now() - startedAt,
probe: compactCapture(lastProbe, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
valuesPrinted: false,
};
}
await sleep(pollMs);
}
return {
ok: false,
payload: { ...lastPayload, ok: false, status: "timeout", degradedReason: "k3s-buildkit-job-timeout", jobName, valuesPrinted: false },
jobName,
create: createPayload,
polls,
elapsedMs: Date.now() - startedAt,
probe: lastProbe === null ? null : compactCapture(lastProbe, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
valuesPrinted: false,
};
}
function yamlLaneK3sBuildProxyEnv(spec: AgentRunLaneSpec): Array<{ name: string; value: string }> {
const build = spec.deployment.manager.imageBuild;
const result: Array<{ name: string; value: string }> = [];
const noProxy = build.noProxy.join(",");
const allProxy = build.httpsProxy ?? build.httpProxy;
if (build.httpProxy !== null) result.push({ name: "HTTP_PROXY", value: build.httpProxy }, { name: "http_proxy", value: build.httpProxy });
if (build.httpsProxy !== null) result.push({ name: "HTTPS_PROXY", value: build.httpsProxy }, { name: "https_proxy", value: build.httpsProxy });
if (allProxy !== null) result.push({ name: "ALL_PROXY", value: allProxy }, { name: "all_proxy", value: allProxy });
result.push({ name: "NO_PROXY", value: noProxy }, { name: "no_proxy", value: noProxy });
return result;
}
function yamlLaneK3sBuildSourceShell(spec: AgentRunLaneSpec, sourceCommit: string): string {
return [
"set -eu",
`read_url=${shQuote(spec.gitMirror.readUrl)}`,
`source_branch=${shQuote(spec.source.branch)}`,
`source_commit=${shQuote(sourceCommit)}`,
"rm -rf /workspace/repo",
"git clone --no-checkout \"$read_url\" /workspace/repo",
"cd /workspace/repo",
"git fetch origin \"refs/heads/$source_branch:refs/remotes/origin/$source_branch\"",
"git checkout --detach \"$source_commit\"",
"actual=$(git rev-parse HEAD)",
"test \"$actual\" = \"$source_commit\"",
"chmod -R a+rwX /workspace",
].join("\n");
}
function yamlLaneK3sBuildImageShell(spec: AgentRunLaneSpec, sourceCommit: string, image: string, envIdentity: string): string {
const build = spec.deployment.manager.imageBuild;
const contextPath = build.context === "." ? "/workspace/repo" : `/workspace/repo/${build.context.replace(/^\.\//u, "")}`;
const buildContainerNoProxy = build.buildContainerProxy.noProxy.join(",");
const buildArgs = [
...Object.entries(build.buildArgs).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `build-arg:${key}=${value}`),
...yamlLaneK3sBuildProxyBuildArgs(build.buildContainerProxy.httpProxy, build.buildContainerProxy.httpsProxy, buildContainerNoProxy),
];
const buildctlArgs = [
"build",
"--allow", "network.host",
"--frontend", "dockerfile.v0",
"--local", `context=${contextPath}`,
"--local", "dockerfile=/workspace/repo",
"--opt", `filename=${build.containerfile}`,
"--opt", `network=${build.network}`,
...buildArgs.flatMap((arg) => ["--opt", arg]),
"--metadata-file", "/workspace/build-metadata.json",
"--output", `type=image,name=${image},push=true,registry.insecure=true`,
];
const repositoryDigestBase = image.slice(0, image.lastIndexOf(":"));
return [
"set -eu",
"cd /workspace/repo",
`buildctl-daemonless.sh ${buildctlArgs.map((arg) => shQuote(arg)).join(" ")}`,
"metadata_compact=$(tr -d '\\n' < /workspace/build-metadata.json)",
"digest=$(printf '%s' \"$metadata_compact\" | sed -n 's/.*\"containerimage.digest\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n 1)",
"if [ -z \"$digest\" ]; then",
` printf '{"ok":false,"status":"failed","failureKind":"image-digest-missing","sourceCommit":"%s","envIdentity":"%s","image":"%s","valuesPrinted":false}\\n' ${shQuote(sourceCommit)} ${shQuote(envIdentity)} ${shQuote(image)}`,
" exit 1",
"fi",
`printf '{"ok":true,"status":"built","sourceCommit":"%s","envIdentity":"%s","image":"%s","digest":"%s","repositoryDigest":"%s@%s","valuesPrinted":false}\\n' ${shQuote(sourceCommit)} ${shQuote(envIdentity)} ${shQuote(image)} "$digest" ${shQuote(repositoryDigestBase)} "$digest"`,
].join("\n");
}
function yamlLaneK3sBuildProxyBuildArgs(httpProxy: string | null, httpsProxy: string | null, noProxy: string): string[] {
const result: string[] = [];
const allProxy = httpsProxy ?? httpProxy;
if (httpProxy !== null) result.push(`build-arg:HTTP_PROXY=${httpProxy}`, `build-arg:http_proxy=${httpProxy}`);
if (httpsProxy !== null) result.push(`build-arg:HTTPS_PROXY=${httpsProxy}`, `build-arg:https_proxy=${httpsProxy}`);
if (allProxy !== null) result.push(`build-arg:ALL_PROXY=${allProxy}`, `build-arg:all_proxy=${allProxy}`);
if (noProxy.length > 0) result.push(`build-arg:NO_PROXY=${noProxy}`, `build-arg:no_proxy=${noProxy}`);
return result;
}
export async function runYamlLaneGitopsPublishJob(config: UniDeskConfig, spec: AgentRunLaneSpec, sourceCommit: string, files: readonly { path: string; content: string }[]): Promise<Record<string, unknown> & { ok: boolean; payload: Record<string, unknown> }> {
const jobName = `gitops-publish-${spec.nodeId.toLowerCase()}-${spec.lane}-${Date.now().toString(36)}`.slice(0, 63);
const manifest = yamlLaneGitopsPublishJobManifest(spec, files, jobName);