@kici-dev/agent 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/checkout/clone-job-repos.d.ts +53 -0
- package/dist/checkout/credential-helper-bin.d.ts +28 -0
- package/dist/checkout/credential-helper-host.d.ts +48 -0
- package/dist/checkout/credential-helper.d.ts +44 -0
- package/dist/checkout/git-clone.d.ts +26 -0
- package/dist/checkout/grant-table.d.ts +30 -0
- package/dist/checkout/job-git-credentials.d.ts +49 -0
- package/dist/checkout/write-elevation.d.ts +40 -0
- package/dist/config.d.ts +38 -0
- package/dist/container-ts-loader-hook.js +2047 -1814
- package/dist/execution/between-jobs-controller.d.ts +50 -0
- package/dist/execution/between-jobs-reset.d.ts +25 -0
- package/dist/execution/cleanup-rerun.d.ts +21 -0
- package/dist/execution/download.d.ts +27 -2
- package/dist/execution/dynamic-job-serializer.d.ts +6 -2
- package/dist/execution/generator-context.d.ts +54 -0
- package/dist/execution/global-eval-runner.d.ts +92 -0
- package/dist/execution/global-workflow-env.d.ts +57 -0
- package/dist/execution/image-build/build-engine.d.ts +75 -0
- package/dist/execution/image-build/build-step.d.ts +57 -0
- package/dist/execution/image-build/resolve-build-spec.d.ts +41 -0
- package/dist/execution/image-build/runtime-facts.d.ts +31 -0
- package/dist/execution/init-runner.d.ts +60 -3
- package/dist/execution/job-runner.d.ts +146 -0
- package/dist/execution/sandbox/bare-metal-sandbox.d.ts +25 -1
- package/dist/execution/sandbox/container-sandbox.d.ts +97 -2
- package/dist/execution/sandbox/fork-runner.d.ts +55 -1
- package/dist/execution/sandbox/image-preflight.d.ts +38 -0
- package/dist/execution/sandbox/ipc-protocol.d.ts +96 -2
- package/dist/execution/sandbox/kici-runtime.d.ts +48 -0
- package/dist/execution/sandbox/step-loop.d.ts +17 -1
- package/dist/execution/sandbox/types.d.ts +55 -1
- package/dist/execution/sandbox/workflow-runner.d.ts +75 -4
- package/dist/execution/workflow-loader.d.ts +8 -1
- package/dist/idle-shutdown.d.ts +24 -0
- package/dist/index.js +221 -80
- package/dist/metrics/prometheus.d.ts +36 -10
- package/dist/server.js +3197 -462
- package/dist/workflow-runner-bundle.js +42207 -41104
- package/dist/workflow-runner.js +594 -196
- package/dist/ws/orchestrator-client.d.ts +95 -3
- package/package.json +10 -10
- package/sbom.spdx.json +464 -454
package/dist/index.js
CHANGED
|
@@ -6,12 +6,12 @@ import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/s
|
|
|
6
6
|
import { KNOWN_ROLES, parseHostPropertyAssignments, validateNoReservedLabels } from "@kici-dev/engine";
|
|
7
7
|
import { execFile } from "node:child_process";
|
|
8
8
|
import { access, cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
9
|
-
import {
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
10
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
11
|
+
import { makeTempDir } from "@kici-dev/core/tmp";
|
|
11
12
|
import { promisify } from "node:util";
|
|
12
13
|
import { createLogger, sha256, toErrorMessage } from "@kici-dev/shared";
|
|
13
14
|
import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, YarnFlavor, detectPackageManagerFromManifests, detectYarnFlavor } from "@kici-dev/shared/package-manager";
|
|
14
|
-
import { existsSync } from "node:fs";
|
|
15
15
|
import { parse, stringify } from "yaml";
|
|
16
16
|
import { Readable, Transform } from "node:stream";
|
|
17
17
|
import { pipeline } from "node:stream/promises";
|
|
@@ -40,6 +40,20 @@ var __exportAll = (all, no_symbols) => {
|
|
|
40
40
|
return target;
|
|
41
41
|
};
|
|
42
42
|
//#endregion
|
|
43
|
+
//#region src/execution/image-build/build-engine.ts
|
|
44
|
+
/**
|
|
45
|
+
* Build a job's container image with the host's build CLI.
|
|
46
|
+
*
|
|
47
|
+
* The CLI is REQUIRED — there is deliberately no socket-API fallback. One build
|
|
48
|
+
* path means one set of Dockerfile semantics: `.dockerignore`, BuildKit and
|
|
49
|
+
* every directive behave as they do on the author's own machine, instead of
|
|
50
|
+
* depending on which agent happened to pick the job up. A host without a CLI
|
|
51
|
+
* cannot run a Dockerfile job, and says so in as many words.
|
|
52
|
+
*/
|
|
53
|
+
/** The build CLIs an agent host may provide. */
|
|
54
|
+
const ContainerBuildCli = z.enum(["docker", "podman"]);
|
|
55
|
+
ContainerBuildCli.enum.docker, ContainerBuildCli.enum.podman;
|
|
56
|
+
//#endregion
|
|
43
57
|
//#region src/config.ts
|
|
44
58
|
/** Execution mode for the agent's sandbox backend. Mirrors the runtime enum. */
|
|
45
59
|
const ExecutionMode = z.enum([
|
|
@@ -47,6 +61,71 @@ const ExecutionMode = z.enum([
|
|
|
47
61
|
"bare-metal",
|
|
48
62
|
"firecracker"
|
|
49
63
|
]);
|
|
64
|
+
/** When the operator between-jobs reset command runs. */
|
|
65
|
+
const BetweenJobsRunOn = z.enum(["always", "on-failure"]);
|
|
66
|
+
const configSchema = z.object({
|
|
67
|
+
orchestratorUrl: z.string().url().min(1, "KICI_ORCHESTRATOR_URL is required"),
|
|
68
|
+
agentId: z.string().optional(),
|
|
69
|
+
labels: z.string().default("").transform((s) => s.split(",").filter(Boolean)),
|
|
70
|
+
properties: z.string().default("").transform((s) => parseHostPropertyAssignments(s.split(",").filter(Boolean))),
|
|
71
|
+
roles: z.string().optional().transform((s) => {
|
|
72
|
+
if (s === void 0) return void 0;
|
|
73
|
+
if (s === "") return [];
|
|
74
|
+
return s.split(",").filter(Boolean);
|
|
75
|
+
}).refine((roles) => {
|
|
76
|
+
if (roles === void 0) return true;
|
|
77
|
+
const validValues = [...KNOWN_ROLES, "all"];
|
|
78
|
+
return roles.every((r) => validValues.includes(r));
|
|
79
|
+
}, { message: `KICI_ROLES must contain only: ${[...KNOWN_ROLES, "all"].join(", ")}` }).transform((roles) => {
|
|
80
|
+
if (roles === void 0) return void 0;
|
|
81
|
+
if (roles.includes("all")) return void 0;
|
|
82
|
+
if (roles.length === 0) return [];
|
|
83
|
+
return roles.filter((r) => r !== "all");
|
|
84
|
+
}),
|
|
85
|
+
port: z.coerce.number().default(8080),
|
|
86
|
+
logLevel: z.enum([
|
|
87
|
+
"debug",
|
|
88
|
+
"info",
|
|
89
|
+
"warn",
|
|
90
|
+
"error"
|
|
91
|
+
]).default("info"),
|
|
92
|
+
agentToken: z.string().optional(),
|
|
93
|
+
githubToken: z.string().optional(),
|
|
94
|
+
maxLogSizeBytes: z.coerce.number().default(10485760),
|
|
95
|
+
defaultStepTimeoutMs: z.coerce.number().default(18e5),
|
|
96
|
+
dockerKeepFailed: z.string().default("false").transform((s) => s === "true"),
|
|
97
|
+
jobHeartbeatIntervalMs: z.coerce.number().default(6e4),
|
|
98
|
+
backpressureMode: z.enum(["pause", "drop"]).default("pause"),
|
|
99
|
+
agentPayloadDir: z.string().optional(),
|
|
100
|
+
agentCommand: z.string().optional(),
|
|
101
|
+
sandbox: z.string().default("false").transform((s) => s === "true"),
|
|
102
|
+
trustedEnv: z.string().default("false").transform((s) => s === "true"),
|
|
103
|
+
inPlace: z.string().default("false").transform((s) => s === "true"),
|
|
104
|
+
sandboxNetwork: z.enum(["isolated", "host"]).default("isolated"),
|
|
105
|
+
sandboxHardened: z.string().default("true").transform((s) => s !== "false"),
|
|
106
|
+
sandboxReadonlyRootfs: z.string().default("false").transform((s) => s === "true"),
|
|
107
|
+
sandboxUser: z.string().optional(),
|
|
108
|
+
sandboxPidsLimit: z.coerce.number().int().positive().default(512),
|
|
109
|
+
sandboxMemoryBytes: z.coerce.number().int().positive().default(2147483648),
|
|
110
|
+
sandboxNanoCpus: z.coerce.number().int().positive().default(2e9),
|
|
111
|
+
scalerManaged: z.string().optional().transform((s) => s === "1"),
|
|
112
|
+
jobImageAgent: z.string().optional().transform((s) => s === "1"),
|
|
113
|
+
runtimeImage: z.string().optional(),
|
|
114
|
+
runtimeNodeSource: z.string().optional(),
|
|
115
|
+
containerBuildCli: ContainerBuildCli.optional(),
|
|
116
|
+
scalerClaimCode: z.string().optional(),
|
|
117
|
+
scalerIdleTimeoutMs: z.coerce.number().default(5e3),
|
|
118
|
+
scalerPendingDispatchTimeoutMs: z.coerce.number().default(6e4),
|
|
119
|
+
executionMode: ExecutionMode.optional(),
|
|
120
|
+
otelExporterOtlpEndpoint: z.string().optional(),
|
|
121
|
+
concurrencyWaitTimeoutMs: z.coerce.number().int().min(1e3).default(36e5),
|
|
122
|
+
isOrchestratorHost: z.string().optional().transform((v) => v === "true"),
|
|
123
|
+
betweenJobsResetCommand: z.string().optional(),
|
|
124
|
+
betweenJobsResetTimeoutMs: z.coerce.number().int().positive().default(6e4),
|
|
125
|
+
betweenJobsResetRunOn: BetweenJobsRunOn.default("always"),
|
|
126
|
+
orphanCleanup: z.string().default("true").transform((s) => s !== "false"),
|
|
127
|
+
drainOnResetFailure: z.string().default("false").transform((s) => s === "true")
|
|
128
|
+
});
|
|
50
129
|
/**
|
|
51
130
|
* Env-var definition for the agent. Exported so the docs generator and the
|
|
52
131
|
* deploy-stg pre-validator can inspect / re-parse without round-tripping
|
|
@@ -54,59 +133,7 @@ const ExecutionMode = z.enum([
|
|
|
54
133
|
*/
|
|
55
134
|
const envDef = defineEnv({
|
|
56
135
|
service: "agent",
|
|
57
|
-
schema:
|
|
58
|
-
orchestratorUrl: z.string().url().min(1, "KICI_ORCHESTRATOR_URL is required"),
|
|
59
|
-
agentId: z.string().optional(),
|
|
60
|
-
labels: z.string().default("").transform((s) => s.split(",").filter(Boolean)),
|
|
61
|
-
properties: z.string().default("").transform((s) => parseHostPropertyAssignments(s.split(",").filter(Boolean))),
|
|
62
|
-
roles: z.string().optional().transform((s) => {
|
|
63
|
-
if (s === void 0) return void 0;
|
|
64
|
-
if (s === "") return [];
|
|
65
|
-
return s.split(",").filter(Boolean);
|
|
66
|
-
}).refine((roles) => {
|
|
67
|
-
if (roles === void 0) return true;
|
|
68
|
-
const validValues = [...KNOWN_ROLES, "all"];
|
|
69
|
-
return roles.every((r) => validValues.includes(r));
|
|
70
|
-
}, { message: `KICI_ROLES must contain only: ${[...KNOWN_ROLES, "all"].join(", ")}` }).transform((roles) => {
|
|
71
|
-
if (roles === void 0) return void 0;
|
|
72
|
-
if (roles.includes("all")) return void 0;
|
|
73
|
-
if (roles.length === 0) return [];
|
|
74
|
-
return roles.filter((r) => r !== "all");
|
|
75
|
-
}),
|
|
76
|
-
port: z.coerce.number().default(8080),
|
|
77
|
-
logLevel: z.enum([
|
|
78
|
-
"debug",
|
|
79
|
-
"info",
|
|
80
|
-
"warn",
|
|
81
|
-
"error"
|
|
82
|
-
]).default("info"),
|
|
83
|
-
agentToken: z.string().optional(),
|
|
84
|
-
githubToken: z.string().optional(),
|
|
85
|
-
maxLogSizeBytes: z.coerce.number().default(10 * 1024 * 1024),
|
|
86
|
-
defaultStepTimeoutMs: z.coerce.number().default(1800 * 1e3),
|
|
87
|
-
dockerKeepFailed: z.string().default("false").transform((s) => s === "true"),
|
|
88
|
-
jobHeartbeatIntervalMs: z.coerce.number().default(6e4),
|
|
89
|
-
backpressureMode: z.enum(["pause", "drop"]).default("pause"),
|
|
90
|
-
agentPayloadDir: z.string().optional(),
|
|
91
|
-
agentCommand: z.string().optional(),
|
|
92
|
-
sandbox: z.string().default("false").transform((s) => s === "true"),
|
|
93
|
-
trustedEnv: z.string().default("false").transform((s) => s === "true"),
|
|
94
|
-
inPlace: z.string().default("false").transform((s) => s === "true"),
|
|
95
|
-
sandboxNetwork: z.enum(["isolated", "host"]).default("isolated"),
|
|
96
|
-
sandboxHardened: z.string().default("true").transform((s) => s !== "false"),
|
|
97
|
-
sandboxReadonlyRootfs: z.string().default("false").transform((s) => s === "true"),
|
|
98
|
-
sandboxUser: z.string().optional(),
|
|
99
|
-
sandboxPidsLimit: z.coerce.number().int().positive().default(512),
|
|
100
|
-
sandboxMemoryBytes: z.coerce.number().int().positive().default(2 * 1024 * 1024 * 1024),
|
|
101
|
-
sandboxNanoCpus: z.coerce.number().int().positive().default(2 * 1e9),
|
|
102
|
-
scalerManaged: z.string().optional().transform((s) => s === "1"),
|
|
103
|
-
scalerIdleTimeoutMs: z.coerce.number().default(5e3),
|
|
104
|
-
scalerPendingDispatchTimeoutMs: z.coerce.number().default(6e4),
|
|
105
|
-
executionMode: ExecutionMode.optional(),
|
|
106
|
-
otelExporterOtlpEndpoint: z.string().optional(),
|
|
107
|
-
concurrencyWaitTimeoutMs: z.coerce.number().int().min(1e3).default(36e5),
|
|
108
|
-
isOrchestratorHost: z.string().optional().transform((v) => v === "true")
|
|
109
|
-
}),
|
|
136
|
+
schema: configSchema,
|
|
110
137
|
envMap: {
|
|
111
138
|
orchestratorUrl: "KICI_ORCHESTRATOR_URL",
|
|
112
139
|
agentId: "KICI_AGENT_ID",
|
|
@@ -136,11 +163,21 @@ const envDef = defineEnv({
|
|
|
136
163
|
sandboxMemoryBytes: "KICI_SANDBOX_MEMORY_BYTES",
|
|
137
164
|
sandboxNanoCpus: "KICI_SANDBOX_NANO_CPUS",
|
|
138
165
|
scalerManaged: "KICI_SCALER_MANAGED",
|
|
166
|
+
jobImageAgent: "KICI_JOB_IMAGE_AGENT",
|
|
167
|
+
runtimeImage: "KICI_RUNTIME_IMAGE",
|
|
168
|
+
runtimeNodeSource: "KICI_RUNTIME_NODE_SOURCE",
|
|
169
|
+
containerBuildCli: "KICI_CONTAINER_BUILD_CLI",
|
|
170
|
+
scalerClaimCode: "KICI_SCALER_CLAIM_CODE",
|
|
139
171
|
scalerIdleTimeoutMs: "KICI_SCALER_IDLE_TIMEOUT",
|
|
140
172
|
scalerPendingDispatchTimeoutMs: "KICI_SCALER_PENDING_DISPATCH_TIMEOUT",
|
|
141
173
|
executionMode: "KICI_EXECUTION_MODE",
|
|
142
174
|
otelExporterOtlpEndpoint: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
143
|
-
concurrencyWaitTimeoutMs: "KICI_CONCURRENCY_WAIT_TIMEOUT_MS"
|
|
175
|
+
concurrencyWaitTimeoutMs: "KICI_CONCURRENCY_WAIT_TIMEOUT_MS",
|
|
176
|
+
betweenJobsResetCommand: "KICI_AGENT_BETWEEN_JOBS_RESET_COMMAND",
|
|
177
|
+
betweenJobsResetTimeoutMs: "KICI_AGENT_BETWEEN_JOBS_RESET_TIMEOUT_MS",
|
|
178
|
+
betweenJobsResetRunOn: "KICI_AGENT_BETWEEN_JOBS_RESET_RUN_ON",
|
|
179
|
+
orphanCleanup: "KICI_AGENT_ORPHAN_CLEANUP",
|
|
180
|
+
drainOnResetFailure: "KICI_AGENT_DRAIN_ON_RESET_FAILURE"
|
|
144
181
|
}
|
|
145
182
|
});
|
|
146
183
|
/**
|
|
@@ -172,10 +209,16 @@ const envDef = defineEnv({
|
|
|
172
209
|
* - KICI_SANDBOX_MEMORY_BYTES (default: 2 GiB) — memory cap in bytes for the job container cgroup
|
|
173
210
|
* - KICI_SANDBOX_NANO_CPUS (default: 2 CPUs) — CPU cap in nano-CPUs for the job container cgroup
|
|
174
211
|
* - KICI_SCALER_MANAGED (set to "1" by the orchestrator's auto-scaler — agent self-shuts down on idle)
|
|
212
|
+
* - KICI_SCALER_CLAIM_CODE (optional single-use claim code the agent exchanges for its own ephemeral credentials before registering; ignored when KICI_AGENT_TOKEN is set)
|
|
175
213
|
* - KICI_SCALER_IDLE_TIMEOUT (ms, default 5000) — how long a scaler-managed agent waits before shutdown after going idle
|
|
176
214
|
* - KICI_SCALER_PENDING_DISPATCH_TIMEOUT (ms, default 60000) — extended idle window when register.ack signals a queued bound job
|
|
177
215
|
* - KICI_EXECUTION_MODE (optional, options: container | bare-metal | firecracker) — override the runner's mode-pick logic
|
|
178
216
|
* - KICI_CONCURRENCY_WAIT_TIMEOUT_MS (default: 3_600_000) — workflow-runner timeout when long-polling for a slot-release follow-up `concurrency.ack`
|
|
217
|
+
* - KICI_AGENT_BETWEEN_JOBS_RESET_COMMAND (optional) — host-reset command run between jobs on a reused agent (fail-open)
|
|
218
|
+
* - KICI_AGENT_BETWEEN_JOBS_RESET_TIMEOUT_MS (default: 60000) — reset command timeout
|
|
219
|
+
* - KICI_AGENT_BETWEEN_JOBS_RESET_RUN_ON (default: always, options: always | on-failure) — when the reset command runs
|
|
220
|
+
* - KICI_AGENT_ORPHAN_CLEANUP (default: true) — reap a finished job's leaked process tree (bare-metal); false = only signal the runner child
|
|
221
|
+
* - KICI_AGENT_DRAIN_ON_RESET_FAILURE (default: false) — drain the agent after repeated consecutive reset failures
|
|
179
222
|
*/
|
|
180
223
|
function loadConfig() {
|
|
181
224
|
const data = envDef.parse();
|
|
@@ -748,11 +791,11 @@ function isAbsoluteRel(rel) {
|
|
|
748
791
|
* env vars — the install runs with `--ignore-scripts` whenever a private
|
|
749
792
|
* registry is configured.
|
|
750
793
|
*/
|
|
751
|
-
const logger$
|
|
794
|
+
const logger$3 = createLogger({ prefix: "dep-installer" });
|
|
752
795
|
const execFileAsync = promisify(execFile);
|
|
753
796
|
/** Install subprocess timeout (10 min) and stdout/stderr buffer (128 MiB). */
|
|
754
797
|
const INSTALL_TIMEOUT_MS = 6e5;
|
|
755
|
-
const INSTALL_MAX_BUFFER =
|
|
798
|
+
const INSTALL_MAX_BUFFER = 134217728;
|
|
756
799
|
/**
|
|
757
800
|
* Detect the package manager for the cloned repo from its committed manifests.
|
|
758
801
|
* A pnpm workspace's `packageManager` field + `pnpm-lock.yaml` live at the repo
|
|
@@ -793,7 +836,7 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
793
836
|
const repoRoot = opts.repoRoot ?? dirname(kiciDir);
|
|
794
837
|
const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
|
|
795
838
|
const yarnFlavor = packageManager === PackageManager.Yarn ? await detectKiciYarnFlavor(repoRoot, kiciDir) : YarnFlavor.Classic;
|
|
796
|
-
logger$
|
|
839
|
+
logger$3.info("Installing deps inline", {
|
|
797
840
|
packageManager,
|
|
798
841
|
yarnFlavor,
|
|
799
842
|
dir: kiciDir
|
|
@@ -851,7 +894,7 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
851
894
|
if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor);
|
|
852
895
|
const durationMs = Date.now() - startTime;
|
|
853
896
|
process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
|
|
854
|
-
logger$
|
|
897
|
+
logger$3.info("Deps installed inline", {
|
|
855
898
|
packageManager,
|
|
856
899
|
durationMs
|
|
857
900
|
});
|
|
@@ -1143,10 +1186,25 @@ var init_dep_restore = __esmMin((() => {
|
|
|
1143
1186
|
* dep-restore.ts and workflow-loader.ts.
|
|
1144
1187
|
*/
|
|
1145
1188
|
var download_exports = /* @__PURE__ */ __exportAll({
|
|
1189
|
+
UPLOAD_MAX_RETRIES: () => 2,
|
|
1146
1190
|
downloadUrl: () => downloadUrl,
|
|
1147
1191
|
uploadToPresignedUrl: () => uploadToPresignedUrl
|
|
1148
1192
|
});
|
|
1149
1193
|
/**
|
|
1194
|
+
* Whether a failed upload attempt is worth repeating.
|
|
1195
|
+
*
|
|
1196
|
+
* A transport failure (connection refused, reset, DNS) never reached a
|
|
1197
|
+
* responder, and 5xx / 429 are the object-storage overload signals AWS
|
|
1198
|
+
* documents as retry-with-backoff (S3 answers `SlowDown` with 503). Every other
|
|
1199
|
+
* status is a decision the server will repeat: a 403 from an expired or
|
|
1200
|
+
* malformed signature, a 400 from a malformed request. Retrying those burns the
|
|
1201
|
+
* ceiling without a chance of success and delays the real error.
|
|
1202
|
+
*/
|
|
1203
|
+
function isRetryableUploadFailure(err) {
|
|
1204
|
+
if (!(err instanceof PresignedUploadHttpError)) return true;
|
|
1205
|
+
return err.statusCode >= 500 || err.statusCode === 429;
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1150
1208
|
* Download content from an HTTP/HTTPS URL.
|
|
1151
1209
|
*
|
|
1152
1210
|
* Includes a 5-minute timeout to prevent the agent from hanging indefinitely
|
|
@@ -1170,21 +1228,10 @@ function downloadUrl(url) {
|
|
|
1170
1228
|
}).on("error", reject);
|
|
1171
1229
|
});
|
|
1172
1230
|
}
|
|
1173
|
-
/**
|
|
1174
|
-
|
|
1175
|
-
*
|
|
1176
|
-
* Used for direct-to-S3 uploads of bundles and dep tarballs. Localhost /
|
|
1177
|
-
* 127.0.0.1 URLs are rewritten via `resolveOrchestratorUrl` so the
|
|
1178
|
-
* filesystem cache backend's signed URLs work from container agents that
|
|
1179
|
-
* can't reach the orchestrator's host loopback directly.
|
|
1180
|
-
*
|
|
1181
|
-
* @param url - The pre-signed URL to upload to
|
|
1182
|
-
* @param data - The buffer to upload
|
|
1183
|
-
*/
|
|
1184
|
-
function uploadToPresignedUrl(url, data) {
|
|
1231
|
+
/** One PUT of the whole buffer. Rejects with {@link PresignedUploadHttpError} on a non-2xx. */
|
|
1232
|
+
function putOnce(resolvedUrl, data, timeoutMs) {
|
|
1185
1233
|
return new Promise((resolve, reject) => {
|
|
1186
|
-
const
|
|
1187
|
-
const parsed = new URL(resolved);
|
|
1234
|
+
const parsed = new URL(resolvedUrl);
|
|
1188
1235
|
const req = (parsed.protocol === "https:" ? https : http).request({
|
|
1189
1236
|
hostname: parsed.hostname,
|
|
1190
1237
|
port: parsed.port,
|
|
@@ -1193,7 +1240,7 @@ function uploadToPresignedUrl(url, data) {
|
|
|
1193
1240
|
headers: { "Content-Length": data.length }
|
|
1194
1241
|
}, (res) => {
|
|
1195
1242
|
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
1196
|
-
reject(
|
|
1243
|
+
reject(new PresignedUploadHttpError(res.statusCode));
|
|
1197
1244
|
res.resume();
|
|
1198
1245
|
return;
|
|
1199
1246
|
}
|
|
@@ -1201,14 +1248,80 @@ function uploadToPresignedUrl(url, data) {
|
|
|
1201
1248
|
res.on("end", () => resolve());
|
|
1202
1249
|
res.on("error", reject);
|
|
1203
1250
|
});
|
|
1251
|
+
req.setTimeout(timeoutMs, () => {
|
|
1252
|
+
req.destroy(/* @__PURE__ */ new Error(`Pre-signed upload timed out after ${timeoutMs}ms`));
|
|
1253
|
+
});
|
|
1204
1254
|
req.on("error", reject);
|
|
1205
1255
|
req.end(data);
|
|
1206
1256
|
});
|
|
1207
1257
|
}
|
|
1208
|
-
|
|
1258
|
+
/**
|
|
1259
|
+
* Upload a buffer to a pre-signed S3 URL via HTTP PUT, retrying a transient
|
|
1260
|
+
* failure.
|
|
1261
|
+
*
|
|
1262
|
+
* Used for direct-to-S3 uploads of bundles and dep tarballs. Localhost /
|
|
1263
|
+
* 127.0.0.1 URLs are rewritten via `resolveOrchestratorUrl` so the
|
|
1264
|
+
* filesystem cache backend's signed URLs work from container agents that
|
|
1265
|
+
* can't reach the orchestrator's host loopback directly.
|
|
1266
|
+
*
|
|
1267
|
+
* **Why retrying is safe here.** A pre-signed PUT writes one whole object at a
|
|
1268
|
+
* single key: there is no multipart session, no append, and no
|
|
1269
|
+
* server-generated identity, so a repeat attempt writes the same bytes to the
|
|
1270
|
+
* same key and the last write wins. S3 also only makes an object visible once
|
|
1271
|
+
* the body has been received in full, so an attempt that died mid-body left
|
|
1272
|
+
* nothing behind. A retry therefore cannot double-write or produce a torn
|
|
1273
|
+
* object — which is why every AWS SDK retries PUTs by default.
|
|
1274
|
+
*
|
|
1275
|
+
* Only a failure that can plausibly differ next time is repeated — see
|
|
1276
|
+
* {@link isRetryableUploadFailure}.
|
|
1277
|
+
*
|
|
1278
|
+
* @param url - The pre-signed URL to upload to
|
|
1279
|
+
* @param data - The buffer to upload
|
|
1280
|
+
* @param opts.baseDelayMs - Backoff before the first retry (doubles thereafter)
|
|
1281
|
+
* @param opts.timeoutMs - Per-attempt socket-inactivity timeout (see
|
|
1282
|
+
* {@link UPLOAD_TIMEOUT_MS}); an override exists so a test can drive the
|
|
1283
|
+
* stall path without waiting out the production budget.
|
|
1284
|
+
*/
|
|
1285
|
+
async function uploadToPresignedUrl(url, data, opts) {
|
|
1286
|
+
const resolved = resolveOrchestratorUrl(url);
|
|
1287
|
+
const baseDelayMs = opts?.baseDelayMs ?? UPLOAD_RETRY_BASE_DELAY_MS;
|
|
1288
|
+
const timeoutMs = opts?.timeoutMs ?? UPLOAD_TIMEOUT_MS;
|
|
1289
|
+
let lastError;
|
|
1290
|
+
for (let attempt = 0; attempt <= 2; attempt++) {
|
|
1291
|
+
if (attempt > 0) {
|
|
1292
|
+
const delayMs = baseDelayMs * 2 ** (attempt - 1);
|
|
1293
|
+
logger$1.warn("Retrying pre-signed upload", {
|
|
1294
|
+
attempt,
|
|
1295
|
+
delayMs,
|
|
1296
|
+
error: lastError?.message
|
|
1297
|
+
});
|
|
1298
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
1299
|
+
}
|
|
1300
|
+
try {
|
|
1301
|
+
await putOnce(resolved, data, timeoutMs);
|
|
1302
|
+
return;
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
1305
|
+
if (!isRetryableUploadFailure(lastError)) throw lastError;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
throw new Error(`Pre-signed upload failed after 3 attempts: ${lastError?.message}`);
|
|
1309
|
+
}
|
|
1310
|
+
var logger$1, DOWNLOAD_TIMEOUT_MS$1, UPLOAD_TIMEOUT_MS, UPLOAD_RETRY_BASE_DELAY_MS, PresignedUploadHttpError;
|
|
1209
1311
|
var init_download = __esmMin((() => {
|
|
1210
1312
|
init_dep_restore();
|
|
1211
|
-
|
|
1313
|
+
logger$1 = createLogger({ prefix: "agent:download" });
|
|
1314
|
+
DOWNLOAD_TIMEOUT_MS$1 = 3e5;
|
|
1315
|
+
UPLOAD_TIMEOUT_MS = 3e5;
|
|
1316
|
+
UPLOAD_RETRY_BASE_DELAY_MS = 500;
|
|
1317
|
+
PresignedUploadHttpError = class extends Error {
|
|
1318
|
+
statusCode;
|
|
1319
|
+
constructor(statusCode) {
|
|
1320
|
+
super(`HTTP ${statusCode} uploading to pre-signed URL`);
|
|
1321
|
+
this.statusCode = statusCode;
|
|
1322
|
+
this.name = "PresignedUploadHttpError";
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1212
1325
|
}));
|
|
1213
1326
|
//#endregion
|
|
1214
1327
|
//#region src/execution/cache/cache-engine.ts
|
|
@@ -1231,11 +1344,13 @@ var init_download = __esmMin((() => {
|
|
|
1231
1344
|
* right destination (repo entries under `workDir`, home entries under the
|
|
1232
1345
|
* homedir). Extraction lands in a scratch dir first, then moves each group
|
|
1233
1346
|
* into place so a partial restore never leaves half-written paths in the live
|
|
1234
|
-
* tree (mirrors dep-restore).
|
|
1347
|
+
* tree (mirrors dep-restore). The scratch dir honors `KICI_TMPDIR`, which may
|
|
1348
|
+
* sit on a different filesystem from the workspace, so the move falls back to
|
|
1349
|
+
* copy-then-remove on `EXDEV` rather than assuming a same-filesystem rename.
|
|
1235
1350
|
*/
|
|
1236
1351
|
const logger = createLogger({ prefix: "cache-engine" });
|
|
1237
1352
|
/** Download timeout for a presigned cache GET: 5 minutes. */
|
|
1238
|
-
const DOWNLOAD_TIMEOUT_MS =
|
|
1353
|
+
const DOWNLOAD_TIMEOUT_MS = 3e5;
|
|
1239
1354
|
/**
|
|
1240
1355
|
* Resolve a cache path. `~`-prefixed -> home root; otherwise repo-root-relative.
|
|
1241
1356
|
* Rejects absolute paths and `..` escapes so a workflow cannot read or clobber
|
|
@@ -1310,6 +1425,32 @@ async function packCachePaths(workDir, paths, roots) {
|
|
|
1310
1425
|
await cleanup();
|
|
1311
1426
|
}
|
|
1312
1427
|
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Move a tree from `src` to `dest`. Attempts a rename (same-filesystem, cheap)
|
|
1430
|
+
* and falls back to copy-then-remove only on `EXDEV` — the errno `rename(2)`
|
|
1431
|
+
* reports when the scratch dir (which honors `KICI_TMPDIR`) is on a different
|
|
1432
|
+
* filesystem from the destination workspace. `verbatimSymlinks` keeps a cached
|
|
1433
|
+
* symlink graph intact (mirroring packCachePaths), and `preserveTimestamps`
|
|
1434
|
+
* keeps mtimes stable so the fallback is byte-for-byte equivalent to the
|
|
1435
|
+
* rename. Any non-`EXDEV` error propagates so a real permission or corruption
|
|
1436
|
+
* failure is never silently turned into a copy.
|
|
1437
|
+
*/
|
|
1438
|
+
async function moveOrCopy(src, dest) {
|
|
1439
|
+
try {
|
|
1440
|
+
await rename(src, dest);
|
|
1441
|
+
} catch (err) {
|
|
1442
|
+
if (err.code !== "EXDEV") throw err;
|
|
1443
|
+
await cp(src, dest, {
|
|
1444
|
+
recursive: true,
|
|
1445
|
+
verbatimSymlinks: true,
|
|
1446
|
+
preserveTimestamps: true
|
|
1447
|
+
});
|
|
1448
|
+
await rm(src, {
|
|
1449
|
+
recursive: true,
|
|
1450
|
+
force: true
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1313
1454
|
/** Move the extracted `__repo__` / `__home__` groups from a scratch dir into place. */
|
|
1314
1455
|
async function moveAnchoredGroups(scratchDir, workDir, home) {
|
|
1315
1456
|
for (const anchor of await readdir(scratchDir)) {
|
|
@@ -1323,7 +1464,7 @@ async function moveAnchoredGroups(scratchDir, workDir, home) {
|
|
|
1323
1464
|
recursive: true,
|
|
1324
1465
|
force: true
|
|
1325
1466
|
});
|
|
1326
|
-
await
|
|
1467
|
+
await moveOrCopy(join(anchorDir, child), dest);
|
|
1327
1468
|
}
|
|
1328
1469
|
}
|
|
1329
1470
|
}
|
|
@@ -2,40 +2,40 @@
|
|
|
2
2
|
* Total completed jobs.
|
|
3
3
|
* Labels:
|
|
4
4
|
* - status: success | failed | cancelled
|
|
5
|
-
* - scaler: injected by orch-side AgentMetricsAggregator
|
|
5
|
+
* - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
|
|
6
6
|
*/
|
|
7
7
|
export declare const jobsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
8
8
|
/**
|
|
9
9
|
* Currently running jobs.
|
|
10
10
|
* Labels:
|
|
11
|
-
* - scaler: injected by orch-side AgentMetricsAggregator
|
|
11
|
+
* - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
|
|
12
12
|
*/
|
|
13
13
|
export declare const jobsActive: import("@opentelemetry/api").UpDownCounter<import("@opentelemetry/api").Attributes>;
|
|
14
14
|
/**
|
|
15
15
|
* Total completed steps.
|
|
16
16
|
* Labels:
|
|
17
17
|
* - status: success | failed | skipped
|
|
18
|
-
* - scaler: injected by orch-side AgentMetricsAggregator
|
|
18
|
+
* - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
|
|
19
19
|
*/
|
|
20
20
|
export declare const stepsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
21
21
|
/**
|
|
22
22
|
* Step execution duration in seconds.
|
|
23
23
|
* Advisory boundaries cover sub-second steps through 30-minute long-running steps.
|
|
24
24
|
* Labels:
|
|
25
|
-
* - scaler: injected by orch-side AgentMetricsAggregator
|
|
25
|
+
* - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
|
|
26
26
|
*/
|
|
27
27
|
export declare const stepDurationSeconds: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
|
|
28
28
|
/**
|
|
29
29
|
* Git clone duration in seconds.
|
|
30
30
|
* Advisory boundaries cover fast shallow clones through large repo clones.
|
|
31
31
|
* Labels:
|
|
32
|
-
* - scaler: injected by orch-side AgentMetricsAggregator
|
|
32
|
+
* - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
|
|
33
33
|
*/
|
|
34
34
|
export declare const cloneDurationSeconds: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
|
|
35
35
|
/**
|
|
36
36
|
* Total log bytes streamed back to orchestrator.
|
|
37
37
|
* Labels:
|
|
38
|
-
* - scaler: injected by orch-side AgentMetricsAggregator
|
|
38
|
+
* - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
|
|
39
39
|
*/
|
|
40
40
|
export declare const logBytesTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
41
41
|
/**
|
|
@@ -51,7 +51,7 @@ export declare const logBytesTotal: import("@opentelemetry/api").Counter<import(
|
|
|
51
51
|
* Labels:
|
|
52
52
|
* - mode: `pause` (producer paused, no data loss) | `drop` (lines
|
|
53
53
|
* discarded with a `[N lines dropped due to backpressure]` marker)
|
|
54
|
-
* - scaler: injected by orch-side AgentMetricsAggregator
|
|
54
|
+
* - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
|
|
55
55
|
*/
|
|
56
56
|
export declare const logBackpressureEventsTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
57
57
|
/**
|
|
@@ -64,7 +64,7 @@ export declare const logBackpressureEventsTotal: import("@opentelemetry/api").Co
|
|
|
64
64
|
* producer.
|
|
65
65
|
*
|
|
66
66
|
* Labels:
|
|
67
|
-
* - scaler: injected by orch-side AgentMetricsAggregator
|
|
67
|
+
* - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
|
|
68
68
|
*/
|
|
69
69
|
export declare const logLinesDroppedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
70
70
|
/**
|
|
@@ -81,7 +81,7 @@ export declare const logLinesDroppedTotal: import("@opentelemetry/api").Counter<
|
|
|
81
81
|
*
|
|
82
82
|
* Labels:
|
|
83
83
|
* - mode: `pause` | `drop`
|
|
84
|
-
* - scaler: injected by orch-side AgentMetricsAggregator
|
|
84
|
+
* - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
|
|
85
85
|
*/
|
|
86
86
|
export declare const logBackpressureActive: import("@opentelemetry/api").UpDownCounter<import("@opentelemetry/api").Attributes>;
|
|
87
87
|
/**
|
|
@@ -89,7 +89,33 @@ export declare const logBackpressureActive: import("@opentelemetry/api").UpDownC
|
|
|
89
89
|
* Use add(1) for connected, add(-1) for disconnected.
|
|
90
90
|
*
|
|
91
91
|
* Labels:
|
|
92
|
-
* - scaler: injected by orch-side AgentMetricsAggregator
|
|
92
|
+
* - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
|
|
93
93
|
*/
|
|
94
94
|
export declare const connectionStatus: import("@opentelemetry/api").UpDownCounter<import("@opentelemetry/api").Attributes>;
|
|
95
|
+
/**
|
|
96
|
+
* Total operator between-jobs reset command invocations.
|
|
97
|
+
* Labels:
|
|
98
|
+
* - status: success | failed | timeout
|
|
99
|
+
* - scaler: injected orch-side; `stateful` for static agents
|
|
100
|
+
*/
|
|
101
|
+
export declare const betweenJobsResetTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
102
|
+
/**
|
|
103
|
+
* Between-jobs reset command duration in seconds.
|
|
104
|
+
* Labels:
|
|
105
|
+
* - scaler: injected orch-side
|
|
106
|
+
*/
|
|
107
|
+
export declare const betweenJobsResetDurationSeconds: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
|
|
108
|
+
/**
|
|
109
|
+
* Processes killed by the between-jobs process-group reap.
|
|
110
|
+
* Labels:
|
|
111
|
+
* - scaler: injected orch-side
|
|
112
|
+
*/
|
|
113
|
+
export declare const orphansReapedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
114
|
+
/**
|
|
115
|
+
* Out-of-band declared-cleanup re-runs after a hard-killed runner.
|
|
116
|
+
* Labels:
|
|
117
|
+
* - status: success | failed | timeout | skipped
|
|
118
|
+
* - scaler: injected orch-side
|
|
119
|
+
*/
|
|
120
|
+
export declare const orphanCleanupTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
95
121
|
//# sourceMappingURL=prometheus.d.ts.map
|