diff --git a/scripts/src/cicd.ts b/scripts/src/cicd.ts index 035b77d6..40022428 100644 --- a/scripts/src/cicd.ts +++ b/scripts/src/cicd.ts @@ -1971,6 +1971,7 @@ function kubeConfigMapFollowerState(registry: BranchFollowerRegistry, options: P return { ok: true, stateByFollower: {}, present: false, error: "" }; } present = true; + if (result.omitted) continue; if (result.value === null || result.value.length === 0) continue; const parsed = parseJsonObject(result.value); if (parsed === null) { @@ -1987,8 +1988,9 @@ function kubeConfigMapFollowerState(registry: BranchFollowerRegistry, options: P }; } -function kubeConfigMapDataValue(registry: BranchFollowerRegistry, options: ParsedOptions, key: string): { ok: boolean; present: boolean; value: string | null; error: string } { +function kubeConfigMapDataValue(registry: BranchFollowerRegistry, options: ParsedOptions, key: string): { ok: boolean; present: boolean; value: string | null; omitted: boolean; error: string } { const template = `{{ with index .data ${JSON.stringify(key)} }}{{ . }}{{ end }}`; + const maxValueBytes = 4096; const script = [ "set -eu", "tmpdir=$(mktemp -d)", @@ -1996,18 +1998,24 @@ function kubeConfigMapDataValue(registry: BranchFollowerRegistry, options: Parse "trap cleanup EXIT INT TERM", `NAMESPACE=${shQuote(registry.controller.namespace)}`, `CONFIGMAP=${shQuote(registry.controller.stateConfigMapName)}`, - "export NAMESPACE CONFIGMAP", + `MAX_VALUE_BYTES=${maxValueBytes}`, + "export NAMESPACE CONFIGMAP MAX_VALUE_BYTES", `if ! value=$(kubectl -n "$NAMESPACE" get configmap "$CONFIGMAP" -o go-template=${shQuote(template)} 2>"$tmpdir/error"); then`, " error_b64=$(tail -c 800 \"$tmpdir/error\" | base64 | tr -d '\\n')", " if grep -qi 'not found' \"$tmpdir/error\"; then", - " printf '{\"ok\":true,\"present\":false,\"valueB64\":null,\"errorB64\":\"%s\"}' \"$error_b64\"", + " printf '{\"ok\":true,\"present\":false,\"valueB64\":null,\"omitted\":false,\"errorB64\":\"%s\"}' \"$error_b64\"", " exit 0", " fi", - " printf '{\"ok\":false,\"present\":false,\"valueB64\":null,\"errorB64\":\"%s\"}' \"$error_b64\"", + " printf '{\"ok\":false,\"present\":false,\"valueB64\":null,\"omitted\":false,\"errorB64\":\"%s\"}' \"$error_b64\"", + " exit 0", + "fi", + "value_bytes=$(printf '%s' \"$value\" | wc -c | tr -d ' ')", + "if [ \"${value_bytes:-0}\" -gt \"$MAX_VALUE_BYTES\" ]; then", + " printf '{\"ok\":true,\"present\":true,\"valueB64\":null,\"omitted\":true,\"valueBytes\":%s,\"errorB64\":\"\"}' \"$value_bytes\"", " exit 0", "fi", "value_b64=$(printf '%s' \"$value\" | base64 | tr -d '\\n')", - "printf '{\"ok\":true,\"present\":true,\"valueB64\":\"%s\",\"errorB64\":\"\"}' \"$value_b64\"", + "printf '{\"ok\":true,\"present\":true,\"valueB64\":\"%s\",\"omitted\":false,\"valueBytes\":%s,\"errorB64\":\"\"}' \"$value_b64\" \"$value_bytes\"", ].join("\n"); const result = runKubeScript(registry, options, script, "", 10_000); const parsed = result.exitCode === 0 ? parseJsonObject(result.stdout) : null; @@ -2016,6 +2024,7 @@ function kubeConfigMapDataValue(registry: BranchFollowerRegistry, options: Parse ok: false, present: false, value: null, + omitted: false, error: redactText(tailText(result.stderr || result.stdout, 800)), }; } @@ -2023,11 +2032,13 @@ function kubeConfigMapDataValue(registry: BranchFollowerRegistry, options: Parse const error = errorB64.length === 0 ? "" : Buffer.from(errorB64, "base64").toString("utf8"); const ok = parsed.ok === true; const present = parsed.present === true; + const omitted = parsed.omitted === true; const valueB64 = typeof parsed.valueB64 === "string" ? parsed.valueB64 : null; return { ok, present, value: valueB64 === null ? null : Buffer.from(valueB64, "base64").toString("utf8"), + omitted, error: redactText(error), }; } @@ -2089,8 +2100,90 @@ function runKubeScript(registry: BranchFollowerRegistry, options: ParsedOptions, return runCommand([transPath(), registry.controller.kubeRoute, "sh"], repoRoot, { input: `${script}\n`, timeoutMs }); } +function compactFollowerStateForConfigMap(state: FollowerState): Record { + return { + id: state.id, + adapter: state.adapter, + enabled: state.enabled, + phase: state.phase, + source: state.source, + target: state.target, + lastTriggeredSha: state.lastTriggeredSha, + lastSucceededSha: state.lastSucceededSha, + pipelineRun: state.pipelineRun, + inFlightJob: state.inFlightJob, + budgetSource: state.budgetSource, + controller: state.controller, + decision: state.decision, + dryRun: state.dryRun, + updatedAt: state.updatedAt, + warnings: state.warnings.slice(0, 6), + stateFormat: "compact-v1", + command: compactStateCommand(state.command), + }; +} + +function compactStateCommand(command: Record | undefined): Record | undefined { + if (command === undefined) return undefined; + const closeout = asOptionalRecord(command.closeout); + const closeoutSummary = asOptionalRecord(closeout?.summary); + const payload = compactNativePayload(asOptionalRecord(command.payload)) ?? closeoutSummary; + return { + mode: stringOrNull(command.mode) ?? stringOrNull(command.status), + namespace: stringOrNull(command.namespace), + pipelineRun: stringOrNull(command.pipelineRun), + sourceCommit: stringOrNull(command.sourceCommit), + sourceStageRef: stringOrNull(command.sourceStageRef), + wait: command.wait === true ? true : undefined, + pipelineRunCompleted: command.pipelineRunCompleted === true ? true : undefined, + stillRunning: command.stillRunning === true ? true : undefined, + closeout: closeout === null + ? null + : { + ok: closeout.ok === true, + completed: closeout.completed === true, + timedOut: closeout.timedOut === true, + polls: numberOrNull(closeout.polls), + elapsedMs: numberOrNull(closeout.elapsedMs), + summary: closeoutSummary, + statusAuthority: stringOrNull(closeout.statusAuthority), + parsedDownstreamCliOutput: false, + }, + payload, + exitCode: numberOrNull(command.exitCode), + timedOut: command.timedOut === true, + statusAuthority: stringOrNull(command.statusAuthority), + parsedDownstreamCliOutput: false, + }; +} + +function compactNativePayload(payload: Record | null): Record | null { + if (payload === null) return null; + return { + source: compactSourcePayload(asOptionalRecord(payload.source)), + tekton: asOptionalRecord(payload.tekton), + argo: asOptionalRecord(payload.argo), + runtime: asOptionalRecord(payload.runtime), + errors: Array.isArray(payload.errors) ? payload.errors.slice(0, 5) : [], + statusAuthority: stringOrNull(payload.statusAuthority), + parsedDownstreamCliOutput: false, + }; +} + +function compactSourcePayload(source: Record | null): Record | null { + if (source === null) return null; + return { + commit: stringOrNull(source.commit), + branch: stringOrNull(source.branch), + repository: stringOrNull(source.repository), + stageRef: stringOrNull(source.stageRef), + sourceAuthority: stringOrNull(source.sourceAuthority), + repoPath: stringOrNull(source.repoPath), + }; +} + function writeFollowerState(registry: BranchFollowerRegistry, state: FollowerState, options: ParsedOptions): CommandResult { - const json = JSON.stringify(state); + const json = JSON.stringify(compactFollowerStateForConfigMap(state)); const dataPatch = JSON.stringify({ data: { [state.id]: json, _updatedAt: new Date().toISOString(), _specRef: SPEC_REF } }); if (options.controller) { const patchBase64 = Buffer.from(dataPatch, "utf8").toString("base64");