@kici-dev/orchestrator 0.18.0 → 0.20.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/agent/registry.d.ts +22 -3
- package/dist/cli/commands/shared/container-agent-image-check.d.ts +67 -0
- package/dist/cli/commands/shared/versioned-upgrade.d.ts +7 -7
- package/dist/cli/service/instance/manifest.d.ts +30 -4
- package/dist/cli.js +279 -54
- package/dist/dashboard/handler.d.ts +2 -0
- package/dist/pipeline/dispatch-matched-workflow.d.ts +1 -1
- package/dist/pipeline/global-eval-round.d.ts +0 -8
- package/dist/scaler/bare-metal-backend.d.ts +2 -1
- package/dist/scaler/container-hostconfig.d.ts +9 -0
- package/dist/scaler/warm-pool.d.ts +4 -1
- package/dist/server.js +1165 -974
- package/dist/standalone.js +566 -479
- package/dist/ws/execution-status-frame.d.ts +2 -1
- package/dist/ws/job-status-forward-frame.d.ts +30 -0
- package/dist/ws/outbound-frame-guard.d.ts +4 -0
- package/dist/ws/platform-client.d.ts +11 -1
- package/dist/ws/test-relay-handlers.d.ts +15 -5
- package/dist/ws/test-run-log-cursor.d.ts +18 -0
- package/installer-image-digests.json +3 -3
- package/package.json +4 -4
- package/sbom.spdx.json +53 -53
package/dist/cli.js
CHANGED
|
@@ -15246,19 +15246,60 @@ function writeInstallManifest(instanceDir, fresh, launch, opts = {}) {
|
|
|
15246
15246
|
function resolveNpmInstallTarget(spec, component, opts) {
|
|
15247
15247
|
const entryRe = new RegExp(`[/\\\\]@kici-dev[/\\\\]${component}[/\\\\]dist[/\\\\](?:server|standalone)\\.js$`);
|
|
15248
15248
|
const entry = [spec.execPath, ...spec.args].find((a) => entryRe.test(a));
|
|
15249
|
-
if (!entry) return
|
|
15249
|
+
if (!entry) return {
|
|
15250
|
+
ok: false,
|
|
15251
|
+
reason: "no-entry"
|
|
15252
|
+
};
|
|
15250
15253
|
const owningPackage = firstGlobalPackage(entry);
|
|
15251
|
-
if (!owningPackage) return
|
|
15254
|
+
if (!owningPackage) return {
|
|
15255
|
+
ok: false,
|
|
15256
|
+
reason: "no-entry"
|
|
15257
|
+
};
|
|
15252
15258
|
const p = opts.windows ? path.win32 : path.posix;
|
|
15253
|
-
const
|
|
15254
|
-
|
|
15259
|
+
const globalPrefix = globalPrefixOf(entry, opts.windows);
|
|
15260
|
+
if (!globalPrefix) return {
|
|
15261
|
+
ok: false,
|
|
15262
|
+
reason: "not-global",
|
|
15263
|
+
entry
|
|
15264
|
+
};
|
|
15265
|
+
const nodeDir = p.dirname(spec.execPath);
|
|
15266
|
+
const npmPath = p.join(nodeDir, opts.windows ? "npm.cmd" : "npm");
|
|
15267
|
+
const npmCliJs = opts.windows ? p.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js") : p.join(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
|
|
15268
|
+
const npmInvocation = (opts.exists ?? fs.existsSync)(npmCliJs) ? {
|
|
15269
|
+
cmd: spec.execPath,
|
|
15270
|
+
preArgs: [npmCliJs]
|
|
15271
|
+
} : {
|
|
15272
|
+
cmd: npmPath,
|
|
15273
|
+
preArgs: []
|
|
15274
|
+
};
|
|
15255
15275
|
return {
|
|
15256
|
-
|
|
15257
|
-
|
|
15258
|
-
|
|
15276
|
+
ok: true,
|
|
15277
|
+
target: {
|
|
15278
|
+
nodeExecPath: spec.execPath,
|
|
15279
|
+
npmPath,
|
|
15280
|
+
owningPackage,
|
|
15281
|
+
globalPrefix,
|
|
15282
|
+
npmInvocation
|
|
15283
|
+
}
|
|
15259
15284
|
};
|
|
15260
15285
|
}
|
|
15261
15286
|
/**
|
|
15287
|
+
* The npm global prefix holding `entry`, split on its FIRST `node_modules`
|
|
15288
|
+
* segment like {@link firstGlobalPackage}. Windows lays a global install out as
|
|
15289
|
+
* `<prefix>\node_modules\<pkg>`; posix as `<prefix>/lib/node_modules/<pkg>`, so
|
|
15290
|
+
* there the segment before must be `lib`, which tells a global install from a
|
|
15291
|
+
* local `node_modules` tree. Null for anything else.
|
|
15292
|
+
*/
|
|
15293
|
+
function globalPrefixOf(entry, windows) {
|
|
15294
|
+
const p = windows ? path.win32 : path.posix;
|
|
15295
|
+
const segments = entry.split(/[/\\]/);
|
|
15296
|
+
const idx = segments.indexOf("node_modules");
|
|
15297
|
+
if (idx < 1) return null;
|
|
15298
|
+
if (windows) return segments.slice(0, idx).join(p.sep) || null;
|
|
15299
|
+
if (idx < 2 || segments[idx - 1] !== "lib") return null;
|
|
15300
|
+
return segments.slice(0, idx - 1).join(p.sep) || p.sep;
|
|
15301
|
+
}
|
|
15302
|
+
/**
|
|
15262
15303
|
* The package under the FIRST `node_modules` segment (the global root), which
|
|
15263
15304
|
* `npm install -g` must target; `@scope/name` keeps both segments. Null
|
|
15264
15305
|
* without a `node_modules` segment.
|
|
@@ -22300,6 +22341,25 @@ function encryptPrivateKey(privateKey, secretKey) {
|
|
|
22300
22341
|
return encrypt(privateKey.toString("base64"), key, 1, PRIVATE_KEY_AAD).data;
|
|
22301
22342
|
}
|
|
22302
22343
|
createLogger({ prefix: "job-secret-seal" });
|
|
22344
|
+
//#endregion
|
|
22345
|
+
//#region src/agent/agent-version.ts
|
|
22346
|
+
/**
|
|
22347
|
+
* Parse `MAJOR.MINOR.PATCH`, or `null` for a non-version. The `-prerelease`
|
|
22348
|
+
* suffix is dropped rather than ordered: it is a build counter (`0.5.0-9159`),
|
|
22349
|
+
* and strict semver would order it BELOW `0.5.0`, reading every prerelease
|
|
22350
|
+
* agent as too old for a feature it carries.
|
|
22351
|
+
*/
|
|
22352
|
+
function parseVersionBase(version) {
|
|
22353
|
+
const parts = version.trim().split("-")[0].split(".");
|
|
22354
|
+
if (parts.length !== 3) return null;
|
|
22355
|
+
const nums = parts.map((part) => /^\d+$/.test(part) ? Number(part) : NaN);
|
|
22356
|
+
if (nums.some((n) => Number.isNaN(n))) return null;
|
|
22357
|
+
return [
|
|
22358
|
+
nums[0],
|
|
22359
|
+
nums[1],
|
|
22360
|
+
nums[2]
|
|
22361
|
+
];
|
|
22362
|
+
}
|
|
22303
22363
|
runtimeLabel(RuntimeFact.enum.docker), runtimeLabel(RuntimeFact.enum.podman);
|
|
22304
22364
|
//#endregion
|
|
22305
22365
|
//#region src/helpers/run-detached.ts
|
|
@@ -24091,6 +24151,117 @@ async function preflightBareMetalBinaryPaths(args) {
|
|
|
24091
24151
|
Nothing was installed and the service was not restarted. Fix binaryPath and run the upgrade again, or pass ${SKIP_BINARY_PATH_CHECK_FLAG} to upgrade anyway.`);
|
|
24092
24152
|
}
|
|
24093
24153
|
//#endregion
|
|
24154
|
+
//#region src/cli/commands/shared/container-agent-image-check.ts
|
|
24155
|
+
/**
|
|
24156
|
+
* The container-scaler agent image check `kici-admin orchestrator upgrade`
|
|
24157
|
+
* runs before it restarts the service.
|
|
24158
|
+
*
|
|
24159
|
+
* A container scaler spawns each agent from the image its label set names, not
|
|
24160
|
+
* from the upgraded orchestrator. So an upgrade leaves container agents on the
|
|
24161
|
+
* old release when a label set pins an older `kici-agent:<version>`, or names
|
|
24162
|
+
* the moving `:latest` tag with the default `imagePullPolicy: IfNotPresent`: a
|
|
24163
|
+
* host that already pulled `:latest` keeps that image. The check only warns:
|
|
24164
|
+
* it never rewrites the operator's scaler config and never stops the upgrade.
|
|
24165
|
+
*/
|
|
24166
|
+
/** The repository name of the published agent image, under any registry or mirror. */
|
|
24167
|
+
const AGENT_IMAGE_NAME = "kici-agent";
|
|
24168
|
+
/** The moving tag an untagged image reference resolves to. */
|
|
24169
|
+
const LATEST_TAG = "latest";
|
|
24170
|
+
/**
|
|
24171
|
+
* Split `[registry/]path/name[:tag][@digest]`. A `:` before the last `/` is a
|
|
24172
|
+
* registry port, not a tag.
|
|
24173
|
+
*/
|
|
24174
|
+
function parseImageRef(image) {
|
|
24175
|
+
const at = image.indexOf("@");
|
|
24176
|
+
const named = at === -1 ? image : image.slice(0, at);
|
|
24177
|
+
const digest = at === -1 ? null : image.slice(at + 1);
|
|
24178
|
+
const colon = named.lastIndexOf(":");
|
|
24179
|
+
if (colon > named.lastIndexOf("/")) return {
|
|
24180
|
+
repository: named.slice(0, colon),
|
|
24181
|
+
tag: named.slice(colon + 1),
|
|
24182
|
+
digest
|
|
24183
|
+
};
|
|
24184
|
+
return {
|
|
24185
|
+
repository: named,
|
|
24186
|
+
tag: null,
|
|
24187
|
+
digest
|
|
24188
|
+
};
|
|
24189
|
+
}
|
|
24190
|
+
/** True when the repository is the KiCI agent image, from quay.io or a mirror. */
|
|
24191
|
+
function isAgentImage(repository) {
|
|
24192
|
+
return repository.slice(repository.lastIndexOf("/") + 1) === AGENT_IMAGE_NAME;
|
|
24193
|
+
}
|
|
24194
|
+
/** -1, 0 or 1 as `a` is an older, the same or a newer release than `b`; null for a non-version. */
|
|
24195
|
+
function compareReleases(a, b) {
|
|
24196
|
+
const left = parseVersionBase(a.replace(/^v/, ""));
|
|
24197
|
+
const right = parseVersionBase(b.replace(/^v/, ""));
|
|
24198
|
+
if (!left || !right) return null;
|
|
24199
|
+
for (let i = 0; i < 3; i++) if (left[i] !== right[i]) return left[i] < right[i] ? -1 : 1;
|
|
24200
|
+
return 0;
|
|
24201
|
+
}
|
|
24202
|
+
/**
|
|
24203
|
+
* Every container-scaler label set whose agent image an upgrade to
|
|
24204
|
+
* `targetVersion` leaves unchanged. An empty result means each agent image is
|
|
24205
|
+
* pinned to the target (or newer), pulled on every spawn, or not a KiCI agent.
|
|
24206
|
+
*/
|
|
24207
|
+
function checkContainerAgentImages(args) {
|
|
24208
|
+
const { config } = args;
|
|
24209
|
+
const bare = args.targetVersion?.trim().replace(/^v/, "") ?? null;
|
|
24210
|
+
const targetVersion = bare && parseVersionBase(bare) ? bare : null;
|
|
24211
|
+
const where = args.scalerConfigDir ? `${args.scalerConfigPath} (or the file under ${args.scalerConfigDir} that declares it)` : args.scalerConfigPath;
|
|
24212
|
+
const findings = [];
|
|
24213
|
+
for (const entry of config.scalers) {
|
|
24214
|
+
if (entry.type !== ScalerBackendType.enum.container) continue;
|
|
24215
|
+
for (const ls of entry.labelSets) {
|
|
24216
|
+
if (!ls.image) continue;
|
|
24217
|
+
const ref = parseImageRef(ls.image);
|
|
24218
|
+
if (!isAgentImage(ref.repository)) continue;
|
|
24219
|
+
const pinned = targetVersion ? `${ref.repository}:${targetVersion}` : null;
|
|
24220
|
+
const prefix = `container scaler "${entry.name}", label set [${ls.labels.join(", ")}]: image ${ls.image}`;
|
|
24221
|
+
const finding = (issue, message) => ({
|
|
24222
|
+
issue,
|
|
24223
|
+
scaler: entry.name,
|
|
24224
|
+
labels: ls.labels,
|
|
24225
|
+
image: ls.image,
|
|
24226
|
+
message: `${prefix} ${message}`
|
|
24227
|
+
});
|
|
24228
|
+
const policy = ls.imagePullPolicy ?? ImagePullPolicy.enum.IfNotPresent;
|
|
24229
|
+
if ((ref.tag === null || ref.tag === LATEST_TAG) && ref.digest === null && policy === ImagePullPolicy.enum.IfNotPresent) {
|
|
24230
|
+
const pin = pinned ? `set "image: ${pinned}"` : `set image to the kici-agent release you upgrade to (${ref.repository}:<version>)`;
|
|
24231
|
+
findings.push(finding("moving-tag", `with imagePullPolicy IfNotPresent keeps the agent image this host already pulled, so the upgrade does not change the container agents. In ${where}, ${pin} on this label set, or set "imagePullPolicy: Always" to pull ${LATEST_TAG} on every spawn.`));
|
|
24232
|
+
continue;
|
|
24233
|
+
}
|
|
24234
|
+
if (!targetVersion || ref.tag === null) continue;
|
|
24235
|
+
if (compareReleases(ref.tag, targetVersion) === -1) findings.push(finding("older-pin", `is older than the upgrade target ${targetVersion}, so the container agents keep running ${ref.tag}. In ${where}, set "image: ${pinned}" on this label set.`));
|
|
24236
|
+
}
|
|
24237
|
+
}
|
|
24238
|
+
return findings;
|
|
24239
|
+
}
|
|
24240
|
+
/**
|
|
24241
|
+
* Print one warning per container label set whose agent image the upgrade
|
|
24242
|
+
* leaves unchanged. Never throws, and stays silent when the env file or the
|
|
24243
|
+
* scaler config cannot be read: the bare-metal `binaryPath` check, which runs
|
|
24244
|
+
* first on the same files, already reports that.
|
|
24245
|
+
*/
|
|
24246
|
+
async function warnAboutContainerAgentImages(args) {
|
|
24247
|
+
let location;
|
|
24248
|
+
let config;
|
|
24249
|
+
try {
|
|
24250
|
+
location = scalerConfigLocation(fs.readFileSync(args.envFilePath, "utf-8"), args.workingDirectory);
|
|
24251
|
+
if (!location) return;
|
|
24252
|
+
config = await loadScalerConfig(location.configPath, location.configDir);
|
|
24253
|
+
} catch {
|
|
24254
|
+
return;
|
|
24255
|
+
}
|
|
24256
|
+
const findings = checkContainerAgentImages({
|
|
24257
|
+
config,
|
|
24258
|
+
targetVersion: args.targetVersion,
|
|
24259
|
+
scalerConfigPath: location.configPath,
|
|
24260
|
+
...location.configDir ? { scalerConfigDir: location.configDir } : {}
|
|
24261
|
+
});
|
|
24262
|
+
for (const f of findings) args.warn(`Warning: ${f.message}`);
|
|
24263
|
+
}
|
|
24264
|
+
//#endregion
|
|
24094
24265
|
//#region src/cli/commands/shared/versioned-upgrade.ts
|
|
24095
24266
|
/**
|
|
24096
24267
|
* Versioned-directory upgrade shared by the kici-admin upgrade commands: each
|
|
@@ -24100,14 +24271,16 @@ Nothing was installed and the service was not restarted. Fix binaryPath and run
|
|
|
24100
24271
|
*/
|
|
24101
24272
|
init_service();
|
|
24102
24273
|
/**
|
|
24103
|
-
* Check
|
|
24104
|
-
*
|
|
24105
|
-
*
|
|
24274
|
+
* Check an orchestrator's scaler config before the upgrade installs or
|
|
24275
|
+
* restarts anything. `spec` is the unit's launch command: the `kici-agent`
|
|
24276
|
+
* installed with its runtime is the one each bare-metal `binaryPath` should
|
|
24106
24277
|
* name. `expectedVersion` is what that agent runs after the upgrade, when the
|
|
24107
24278
|
* caller knows it. Throws when a `binaryPath` would stop the restarted service,
|
|
24108
|
-
* unless `skip` is set: then every finding is a warning.
|
|
24279
|
+
* unless `skip` is set: then every finding is a warning. Then warns about each
|
|
24280
|
+
* container label set whose agent image stays behind `targetVersion`, the
|
|
24281
|
+
* release the service runs after the upgrade.
|
|
24109
24282
|
*/
|
|
24110
|
-
async function
|
|
24283
|
+
async function checkScalersBeforeUpgrade(args) {
|
|
24111
24284
|
if (args.component !== "orchestrator" || args.platform === "compose") return;
|
|
24112
24285
|
const expectedAgent = args.spec ? agentBinaryFromLaunch(args.spec, isWindows(args.platform)) : null;
|
|
24113
24286
|
await preflightBareMetalBinaryPaths({
|
|
@@ -24120,6 +24293,12 @@ async function checkBareMetalBinaryPathsBeforeUpgrade(args) {
|
|
|
24120
24293
|
skip: args.skip,
|
|
24121
24294
|
warn: (line) => console.warn(line)
|
|
24122
24295
|
});
|
|
24296
|
+
await warnAboutContainerAgentImages({
|
|
24297
|
+
envFilePath: args.config.envFilePath,
|
|
24298
|
+
workingDirectory: args.config.workingDirectory,
|
|
24299
|
+
targetVersion: args.targetVersion,
|
|
24300
|
+
warn: (line) => console.warn(line)
|
|
24301
|
+
});
|
|
24123
24302
|
}
|
|
24124
24303
|
/**
|
|
24125
24304
|
* Resolve the upgrade target through {@link resolveInstanceTarget}: `instanceDir`,
|
|
@@ -24448,14 +24627,18 @@ function planNpmSourceUpgrade(opts) {
|
|
|
24448
24627
|
kind: "error",
|
|
24449
24628
|
reason: "self-driving upgrade requires --version <target> (the version to install). Pass --restart-only to restart the already-installed package without installing."
|
|
24450
24629
|
};
|
|
24451
|
-
const
|
|
24452
|
-
if (!
|
|
24630
|
+
const resolution = spec ? resolveNpmInstallTarget(spec, component, { windows }) : null;
|
|
24631
|
+
if (resolution && !resolution.ok && resolution.reason === "not-global") return {
|
|
24632
|
+
kind: "error",
|
|
24633
|
+
reason: `npm-source upgrade cannot self-install: the unit's launch target ${resolution.entry} is not inside a global npm install, so the unit's npm has no global prefix that holds it. The unit runs ${spec.execPath}; this kici-admin runs ${process.execPath}. Use an archive upgrade (--from <archive> / --url <url> + --version), or pre-install the package and re-run with --restart-only.`
|
|
24634
|
+
};
|
|
24635
|
+
if (!resolution?.ok) return {
|
|
24453
24636
|
kind: "error",
|
|
24454
24637
|
reason: "npm-source upgrade cannot self-install: the unit's launch target is a custom --binary install or a non-node_modules path, so there is no global package to update. Use an archive upgrade (--from <archive> / --url <url> + --version), or pre-install the package and re-run with --restart-only."
|
|
24455
24638
|
};
|
|
24456
24639
|
return {
|
|
24457
24640
|
kind: "self-drive",
|
|
24458
|
-
target,
|
|
24641
|
+
target: resolution.target,
|
|
24459
24642
|
version: requestedVersion
|
|
24460
24643
|
};
|
|
24461
24644
|
}
|
|
@@ -24471,9 +24654,14 @@ function windowsShellCommand(cmd, args) {
|
|
|
24471
24654
|
for (const arg of args) if (!/^[A-Za-z0-9@/._+-]+$/.test(arg)) throw new Error(`cannot pass "${arg}" to npm: it holds a character cmd.exe reads as syntax`);
|
|
24472
24655
|
return [`"${cmd}"`, ...args].join(" ");
|
|
24473
24656
|
}
|
|
24657
|
+
/**
|
|
24658
|
+
* Only a Windows batch file (`npm.cmd`) goes through cmd.exe; the pinned
|
|
24659
|
+
* `node.exe` running npm-cli.js spawns directly, with no shell to parse its
|
|
24660
|
+
* `C:\…` arguments.
|
|
24661
|
+
*/
|
|
24474
24662
|
const defaultSpawnRunner = (cmd, args, opts) => {
|
|
24475
24663
|
const env = opts?.env ?? process.env;
|
|
24476
|
-
const r = process.platform === "win32" ? spawnSync(windowsShellCommand(cmd, args), {
|
|
24664
|
+
const r = process.platform === "win32" && /\.cmd$/i.test(cmd) ? spawnSync(windowsShellCommand(cmd, args), {
|
|
24477
24665
|
encoding: "utf-8",
|
|
24478
24666
|
shell: true,
|
|
24479
24667
|
env
|
|
@@ -24488,15 +24676,40 @@ const defaultSpawnRunner = (cmd, args, opts) => {
|
|
|
24488
24676
|
};
|
|
24489
24677
|
};
|
|
24490
24678
|
/**
|
|
24491
|
-
* The environment npm runs in: the unit's pinned node first on PATH,
|
|
24492
|
-
*
|
|
24679
|
+
* The environment npm runs in: the unit's pinned node first on PATH, so a
|
|
24680
|
+
* wrapper fallback (`npmPath`, an `env node` shebang) resolves that node. The
|
|
24681
|
+
* caller's `npm_config_*` stay, so an operator-set registry or proxy still
|
|
24682
|
+
* applies.
|
|
24683
|
+
*
|
|
24684
|
+
* On the `npm.cmd` fallback the prefix goes through `npm_config_prefix`:
|
|
24685
|
+
* {@link windowsShellCommand} refuses the `\`, `:` and `=` a `--prefix=C:\…`
|
|
24686
|
+
* argument holds. Every other invocation passes `--prefix` on the command
|
|
24687
|
+
* line ({@link npmArgs}), which npm ranks above the environment.
|
|
24493
24688
|
*/
|
|
24494
24689
|
function npmEnv(target) {
|
|
24495
24690
|
const pinnedBinDir = path.dirname(target.nodeExecPath);
|
|
24496
|
-
|
|
24691
|
+
const env = {
|
|
24497
24692
|
...process.env,
|
|
24498
24693
|
PATH: `${pinnedBinDir}${path.delimiter}${process.env.PATH ?? ""}`
|
|
24499
24694
|
};
|
|
24695
|
+
if (prefixViaEnv(target)) {
|
|
24696
|
+
for (const key of Object.keys(env)) if (/^npm_config_(global_)?prefix$/i.test(key)) delete env[key];
|
|
24697
|
+
env.npm_config_prefix = target.globalPrefix;
|
|
24698
|
+
}
|
|
24699
|
+
return env;
|
|
24700
|
+
}
|
|
24701
|
+
/** Whether the prefix travels in the environment: the `npm.cmd` fallback. */
|
|
24702
|
+
function prefixViaEnv(target) {
|
|
24703
|
+
return target.npmInvocation.preArgs.length === 0 && /\.cmd$/i.test(target.npmInvocation.cmd);
|
|
24704
|
+
}
|
|
24705
|
+
/** The argv for one npm call: npm-cli.js first when the pinned node runs it, `--prefix` last. */
|
|
24706
|
+
function npmArgs(target, args) {
|
|
24707
|
+
const prefix = prefixViaEnv(target) ? [] : [`--prefix=${target.globalPrefix}`];
|
|
24708
|
+
return [
|
|
24709
|
+
...target.npmInvocation.preArgs,
|
|
24710
|
+
...args,
|
|
24711
|
+
...prefix
|
|
24712
|
+
];
|
|
24500
24713
|
}
|
|
24501
24714
|
/**
|
|
24502
24715
|
* Whether `version` reads KICI_ENV_FILE, asked of the unit's npm registry. Such
|
|
@@ -24504,12 +24717,12 @@ function npmEnv(target) {
|
|
|
24504
24717
|
* KiCI package of that version depends on.
|
|
24505
24718
|
*/
|
|
24506
24719
|
function npmReleaseReadsEnvFile(target, version, run = defaultSpawnRunner) {
|
|
24507
|
-
const res = run(target.
|
|
24720
|
+
const res = run(target.npmInvocation.cmd, npmArgs(target, [
|
|
24508
24721
|
"view",
|
|
24509
24722
|
`@kici-dev/shared@${version}`,
|
|
24510
24723
|
"exports",
|
|
24511
24724
|
"--json"
|
|
24512
|
-
], { env: npmEnv(target) });
|
|
24725
|
+
]), { env: npmEnv(target) });
|
|
24513
24726
|
if (res.status !== 0) return {
|
|
24514
24727
|
ok: false,
|
|
24515
24728
|
error: res.stderr.trim() || `npm view exited with ${res.status}`
|
|
@@ -24538,20 +24751,20 @@ function npmReleaseReadsEnvFile(target, version, run = defaultSpawnRunner) {
|
|
|
24538
24751
|
};
|
|
24539
24752
|
}
|
|
24540
24753
|
/**
|
|
24541
|
-
* Install `<owningPackage>@<version>` globally
|
|
24542
|
-
*
|
|
24543
|
-
*
|
|
24544
|
-
*
|
|
24545
|
-
*
|
|
24546
|
-
*
|
|
24547
|
-
*
|
|
24754
|
+
* Install `<owningPackage>@<version>` globally into the prefix the unit's
|
|
24755
|
+
* ExecStart resolves from, with the unit's pinned node. Without an explicit
|
|
24756
|
+
* prefix npm derives one from the node that runs npm-cli.js, or takes the
|
|
24757
|
+
* `npm_config_prefix` the caller's environment carries (`npx` exports its
|
|
24758
|
+
* own), so the install would land where the unit never reads it. The pinned
|
|
24759
|
+
* node runs npm-cli.js directly, skipping a version-manager wrapper at
|
|
24760
|
+
* `<pinned>/bin/npm`, which also keeps the shell's active node out of it.
|
|
24548
24761
|
*/
|
|
24549
24762
|
function installGlobalPackage(target, version, run = defaultSpawnRunner) {
|
|
24550
|
-
const res = run(target.
|
|
24763
|
+
const res = run(target.npmInvocation.cmd, npmArgs(target, [
|
|
24551
24764
|
"install",
|
|
24552
24765
|
"-g",
|
|
24553
24766
|
`${target.owningPackage}@${version}`
|
|
24554
|
-
], { env: npmEnv(target) });
|
|
24767
|
+
]), { env: npmEnv(target) });
|
|
24555
24768
|
return {
|
|
24556
24769
|
ok: res.status === 0,
|
|
24557
24770
|
stderr: res.stderr
|
|
@@ -24673,12 +24886,13 @@ async function performVersionedUpgrade(component, opts, hooks = {}) {
|
|
|
24673
24886
|
console.error(`Error: service "${config.name}" is not installed`);
|
|
24674
24887
|
process.exit(1);
|
|
24675
24888
|
}
|
|
24676
|
-
await
|
|
24889
|
+
await checkScalersBeforeUpgrade({
|
|
24677
24890
|
component,
|
|
24678
24891
|
config,
|
|
24679
24892
|
platform,
|
|
24680
24893
|
spec: await manager.readLaunchSpec(config),
|
|
24681
24894
|
expectedVersion: null,
|
|
24895
|
+
targetVersion: version,
|
|
24682
24896
|
willInstall: false,
|
|
24683
24897
|
skip: opts.skipBinaryPathCheck ?? false
|
|
24684
24898
|
});
|
|
@@ -24834,12 +25048,13 @@ async function restartOnlyUpgrade(args) {
|
|
|
24834
25048
|
console.error(`Error: ${verdict.reason}`);
|
|
24835
25049
|
process.exit(1);
|
|
24836
25050
|
}
|
|
24837
|
-
await
|
|
25051
|
+
await checkScalersBeforeUpgrade({
|
|
24838
25052
|
component: args.component,
|
|
24839
25053
|
config,
|
|
24840
25054
|
platform,
|
|
24841
25055
|
spec,
|
|
24842
25056
|
expectedVersion: null,
|
|
25057
|
+
targetVersion: verdict.version,
|
|
24843
25058
|
willInstall: false,
|
|
24844
25059
|
skip: opts.skipBinaryPathCheck ?? false
|
|
24845
25060
|
});
|
|
@@ -24870,12 +25085,13 @@ async function restartOnlyUpgrade(args) {
|
|
|
24870
25085
|
async function performSelfDrivingInstall(args) {
|
|
24871
25086
|
const { component, config, resolvedInstance, manager, action, opts, platform, spec } = args;
|
|
24872
25087
|
const { target, version } = action;
|
|
24873
|
-
await
|
|
25088
|
+
await checkScalersBeforeUpgrade({
|
|
24874
25089
|
component,
|
|
24875
25090
|
config,
|
|
24876
25091
|
platform,
|
|
24877
25092
|
spec,
|
|
24878
25093
|
expectedVersion: `${target.owningPackage}@${version}`,
|
|
25094
|
+
targetVersion: version,
|
|
24879
25095
|
willInstall: true,
|
|
24880
25096
|
skip: opts.skipBinaryPathCheck ?? false
|
|
24881
25097
|
});
|
|
@@ -24886,15 +25102,14 @@ async function performSelfDrivingInstall(args) {
|
|
|
24886
25102
|
if (!verdict.reads) throw new Error(`refusing to install ${release} for "${config.name}": ${envFileRefusal(release, config.envFilePath)} The service and the installed package were not changed.`);
|
|
24887
25103
|
}
|
|
24888
25104
|
if (!opts.yes) {
|
|
24889
|
-
console.log(`This will install ${target.owningPackage}@${version}
|
|
24890
|
-
console.log(` (node runtime: ${target.nodeExecPath}), then restart "${config.name}".`);
|
|
25105
|
+
console.log(`This will install ${target.owningPackage}@${version} with ${target.nodeExecPath} into ${target.globalPrefix} (the unit's runtime), then restart "${config.name}".`);
|
|
24891
25106
|
console.log("");
|
|
24892
25107
|
if (!await confirm$1("Proceed with upgrade?")) {
|
|
24893
25108
|
console.log("Upgrade cancelled.");
|
|
24894
25109
|
return;
|
|
24895
25110
|
}
|
|
24896
25111
|
}
|
|
24897
|
-
console.log(`Installing ${target.owningPackage}@${version}
|
|
25112
|
+
console.log(`Installing ${target.owningPackage}@${version} with ${target.nodeExecPath} into ${target.globalPrefix} (the unit's runtime)...`);
|
|
24898
25113
|
const result = installGlobalPackage(target, version, args.run);
|
|
24899
25114
|
if (!result.ok) {
|
|
24900
25115
|
const hint = /EACCES|permission denied|EPERM/i.test(result.stderr) ? " The unit's global prefix is not writable — re-run with privileges matching the unit's runtime." : "";
|
|
@@ -25034,7 +25249,7 @@ async function switchToInstalledVersion(args) {
|
|
|
25034
25249
|
action: `switch "${config.name}" to ${targetVersion}`,
|
|
25035
25250
|
config
|
|
25036
25251
|
});
|
|
25037
|
-
await
|
|
25252
|
+
await warnAboutScalers(args);
|
|
25038
25253
|
if (!await assertSchemaAllowsSwitch({
|
|
25039
25254
|
component,
|
|
25040
25255
|
targetVersion,
|
|
@@ -25068,18 +25283,20 @@ async function switchToInstalledVersion(args) {
|
|
|
25068
25283
|
});
|
|
25069
25284
|
}
|
|
25070
25285
|
/**
|
|
25071
|
-
* Print the bare-metal `binaryPath`
|
|
25072
|
-
* restarts the service. Never refuses and never
|
|
25073
|
-
* paths, and a check that cannot run must not block
|
|
25286
|
+
* Print the bare-metal `binaryPath` and container agent image diagnoses before
|
|
25287
|
+
* `--rollback` or `--pick` restarts the service. Never refuses and never
|
|
25288
|
+
* throws: both are recovery paths, and a check that cannot run must not block
|
|
25289
|
+
* a recovery.
|
|
25074
25290
|
*/
|
|
25075
|
-
async function
|
|
25291
|
+
async function warnAboutScalers(args) {
|
|
25076
25292
|
try {
|
|
25077
|
-
await
|
|
25293
|
+
await checkScalersBeforeUpgrade({
|
|
25078
25294
|
component: args.component,
|
|
25079
25295
|
config: args.config,
|
|
25080
25296
|
platform: args.platform,
|
|
25081
25297
|
spec: await args.manager.readLaunchSpec(args.config).catch(() => null),
|
|
25082
25298
|
expectedVersion: null,
|
|
25299
|
+
targetVersion: args.targetVersion,
|
|
25083
25300
|
willInstall: false,
|
|
25084
25301
|
skip: true
|
|
25085
25302
|
});
|
|
@@ -26603,7 +26820,7 @@ init_manifest();
|
|
|
26603
26820
|
const ALL_PLATFORMS = AgentPlatform.options;
|
|
26604
26821
|
/** The orchestrator's own version (single-version invariant) = the packaged agent version. */
|
|
26605
26822
|
function resolveKiciVersion() {
|
|
26606
|
-
return "0.
|
|
26823
|
+
return "0.20.0";
|
|
26607
26824
|
}
|
|
26608
26825
|
/** Parse the --platform value: default set | single | CSV | `all`. */
|
|
26609
26826
|
function parsePlatforms(raw) {
|
|
@@ -35310,6 +35527,7 @@ function buildAgentContainerHostConfig(input) {
|
|
|
35310
35527
|
...typeof cpus === "number" && cpus > 0 ? { NanoCpus: Math.round(cpus * 1e9) } : {},
|
|
35311
35528
|
...input.binds && input.binds.length > 0 ? { Binds: input.binds } : {},
|
|
35312
35529
|
...input.extraHosts && input.extraHosts.length > 0 ? { ExtraHosts: input.extraHosts } : {},
|
|
35530
|
+
...input.supplementaryGroups && input.supplementaryGroups.length > 0 ? { GroupAdd: input.supplementaryGroups.map(String) } : {},
|
|
35313
35531
|
AutoRemove: false,
|
|
35314
35532
|
PidsLimit: AGENT_CONTAINER_PIDS_LIMIT
|
|
35315
35533
|
};
|
|
@@ -35374,6 +35592,19 @@ async function detectRuntime(runtimeHint) {
|
|
|
35374
35592
|
* Create a LogCapture from a running container's stdout/stderr streams.
|
|
35375
35593
|
* Uses dockerode's demuxStream to strip Docker's 8-byte multiplexed headers.
|
|
35376
35594
|
*/
|
|
35595
|
+
/**
|
|
35596
|
+
* The cpu / memory ceiling for one spawn. `effectiveLimits` from ScalerManager
|
|
35597
|
+
* wins: it already folds in job overrides, label-set and scaler defaults.
|
|
35598
|
+
* Otherwise the label set's (or the scaler's default) `resources.limits` apply.
|
|
35599
|
+
*/
|
|
35600
|
+
function resolveSpawnLimits(effectiveLimits, resources) {
|
|
35601
|
+
if (effectiveLimits && (effectiveLimits.cpus || effectiveLimits.memBytes)) return effectiveLimits;
|
|
35602
|
+
const limits = resources?.limits;
|
|
35603
|
+
return {
|
|
35604
|
+
...limits?.memory ? { memBytes: parseMemoryString(limits.memory) } : {},
|
|
35605
|
+
...limits?.cpus ? { cpus: limits.cpus } : {}
|
|
35606
|
+
};
|
|
35607
|
+
}
|
|
35377
35608
|
async function createContainerLogCapture(docker, containerId) {
|
|
35378
35609
|
const stream = await docker.getContainer(containerId).logs({
|
|
35379
35610
|
follow: true,
|
|
@@ -35661,15 +35892,7 @@ var ContainerScalerBackend = class ContainerScalerBackend {
|
|
|
35661
35892
|
];
|
|
35662
35893
|
const binds = [...matchedLabelSet.volumes ?? []];
|
|
35663
35894
|
if (matchedLabelSet.containerSocket && this.resolvedSocketPath) binds.push(`${this.resolvedSocketPath}:${this.resolvedSocketPath}`);
|
|
35664
|
-
|
|
35665
|
-
if (effectiveLimits && (effectiveLimits.cpus || effectiveLimits.memBytes)) resolvedLimits = effectiveLimits;
|
|
35666
|
-
else {
|
|
35667
|
-
const limits = (matchedLabelSet.resources ?? this.defaultResources)?.limits;
|
|
35668
|
-
resolvedLimits = {
|
|
35669
|
-
...limits?.memory ? { memBytes: parseMemoryString(limits.memory) } : {},
|
|
35670
|
-
...limits?.cpus ? { cpus: limits.cpus } : {}
|
|
35671
|
-
};
|
|
35672
|
-
}
|
|
35895
|
+
const resolvedLimits = resolveSpawnLimits(effectiveLimits, matchedLabelSet.resources ?? this.defaultResources);
|
|
35673
35896
|
const jobContainer = spawnContext?.container;
|
|
35674
35897
|
const spawnImage = jobContainer?.image ?? matchedLabelSet.image;
|
|
35675
35898
|
await pullImageIfMissing({
|
|
@@ -35692,6 +35915,7 @@ var ContainerScalerBackend = class ContainerScalerBackend {
|
|
|
35692
35915
|
binds.push(runtimeInjectBind(runtimeVolume));
|
|
35693
35916
|
agentInit = runtimeInitCommand(await agentImageLabels(this.docker, matchedLabelSet.image));
|
|
35694
35917
|
}
|
|
35918
|
+
if (!jobContainer) env.push("KICI_SCALER_SINGLE_JOB=1");
|
|
35695
35919
|
emit(ScalerEventType.enum["scaler.provisioning"], "creating container");
|
|
35696
35920
|
const normalizedLabelSetStr = normalizeLabelSet(labelSet);
|
|
35697
35921
|
const container = await this.docker.createContainer({
|
|
@@ -35710,7 +35934,8 @@ var ContainerScalerBackend = class ContainerScalerBackend {
|
|
|
35710
35934
|
HostConfig: buildAgentContainerHostConfig({
|
|
35711
35935
|
limits: resolvedLimits,
|
|
35712
35936
|
binds,
|
|
35713
|
-
...this.extraHosts ? { extraHosts: this.extraHosts } : {}
|
|
35937
|
+
...this.extraHosts ? { extraHosts: this.extraHosts } : {},
|
|
35938
|
+
...!jobContainer && { supplementaryGroups: [1990] }
|
|
35714
35939
|
}),
|
|
35715
35940
|
...this.networkIsolation && { NetworkingConfig: { EndpointsConfig: { ["kici-agent-net"]: {} } } }
|
|
35716
35941
|
});
|
|
@@ -36844,7 +37069,7 @@ function routeRootArgs(root, args) {
|
|
|
36844
37069
|
* build entry; the fallback covers a source run (tests, `tsx`), the same shape
|
|
36845
37070
|
* `server.ts` and `standalone.ts` use for `ORCHESTRATOR_VERSION`.
|
|
36846
37071
|
*/
|
|
36847
|
-
const CLI_VERSION = "0.
|
|
37072
|
+
const CLI_VERSION = "0.20.0";
|
|
36848
37073
|
/**
|
|
36849
37074
|
* Root command whose option parsing first routes argv through
|
|
36850
37075
|
* {@link routeRootArgs}: a leading `--version` prints the CLI version, and an
|
|
@@ -156,6 +156,8 @@ export declare class DashboardHandler {
|
|
|
156
156
|
private enforcePolicy;
|
|
157
157
|
/** dashboard.run.detail: the run's nested job/step tree. */
|
|
158
158
|
handleRunDetail(msg: DashboardRunDetailRequest): Promise<void>;
|
|
159
|
+
/** Answer a run-detail read for a run this orchestrator does not hold. */
|
|
160
|
+
private sendRunDetailNotFound;
|
|
159
161
|
/**
|
|
160
162
|
* dashboard.run.structured: the user-plane twin of the orchestrator-admin
|
|
161
163
|
* `/runs/:id/structured` endpoint, byte-identical because it reuses the
|
|
@@ -71,7 +71,7 @@ export interface WorkflowDispatchContext {
|
|
|
71
71
|
fullLockFile: {
|
|
72
72
|
workflows: readonly LockWorkflow[];
|
|
73
73
|
lockfileHash?: string;
|
|
74
|
-
/** In-repo
|
|
74
|
+
/** In-repo workspace sibling closure digest; part of the dep pointer key. */
|
|
75
75
|
siblingsDigest?: string;
|
|
76
76
|
source: {
|
|
77
77
|
file: string;
|
|
@@ -193,14 +193,6 @@ export declare function narrowVerdict(raw: unknown, workflowName: string): Globa
|
|
|
193
193
|
* best-effort post, so the check would vanish.
|
|
194
194
|
*/
|
|
195
195
|
export declare const MAX_ROUND_REASON_CHARS = 4000;
|
|
196
|
-
/**
|
|
197
|
-
* Truncate `text` to at most `max` UTF-16 code units, marking that it was cut.
|
|
198
|
-
*
|
|
199
|
-
* - A `max` no larger than the marker returns the truncated marker, never a
|
|
200
|
-
* string longer than `max`.
|
|
201
|
-
* - The cut backs off one unit rather than split a surrogate pair.
|
|
202
|
-
*/
|
|
203
|
-
export declare function truncateReasonText(text: string, max: number): string;
|
|
204
196
|
/**
|
|
205
197
|
* One round that produced no verdicts after every attempt. Exactly one per
|
|
206
198
|
* failed round, never one per candidate (the round exists to collapse N
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Bare-metal scaler backend: ephemeral agent processes via child_process.spawn.
|
|
3
3
|
* On Linux and macOS each agent leads its own process group, which teardown
|
|
4
4
|
* signals; on Windows teardown ends the agent's process tree with taskkill.
|
|
5
|
-
*
|
|
5
|
+
* An agent exits once it is idle past its idle window (a follow-up job that
|
|
6
|
+
* arrives within the window reuses it), and the backend destroys it on disconnect.
|
|
6
7
|
*/
|
|
7
8
|
import { type ToolRequirement } from '@kici-dev/shared';
|
|
8
9
|
import type { AgentTokenStore } from '../agent/token-store.js';
|
|
@@ -15,6 +15,13 @@ import type { EffectiveLimits } from './types.js';
|
|
|
15
15
|
* its own docs and validation, and nothing yet says the constant is wrong.
|
|
16
16
|
*/
|
|
17
17
|
export declare const AGENT_CONTAINER_PIDS_LIMIT = 4096;
|
|
18
|
+
/**
|
|
19
|
+
* Group id of `kici-sudo` in the KiCI agent image (`packages/agent/Dockerfile`
|
|
20
|
+
* creates it). `/etc/sudoers.d/kici-sudo` grants that group passwordless sudo.
|
|
21
|
+
* The container scaler adds the group only to the single-job containers it
|
|
22
|
+
* starts from a pool's own image, so root never outlives one job.
|
|
23
|
+
*/
|
|
24
|
+
export declare const AGENT_SUDO_GROUP_GID = 1990;
|
|
18
25
|
/**
|
|
19
26
|
* Why this container is NOT capability-dropped, though the job containers the
|
|
20
27
|
* agent starts are.
|
|
@@ -42,6 +49,8 @@ export interface AgentContainerHostConfigInput {
|
|
|
42
49
|
binds?: string[];
|
|
43
50
|
/** Extra `host:ip` entries (e.g. a private registry alias). */
|
|
44
51
|
extraHosts?: string[];
|
|
52
|
+
/** Extra group ids for the container's process (`GroupAdd`). */
|
|
53
|
+
supplementaryGroups?: number[];
|
|
45
54
|
}
|
|
46
55
|
/**
|
|
47
56
|
* Build the agent container's `HostConfig`.
|
|
@@ -19,7 +19,10 @@ export interface WarmPoolCallbacks {
|
|
|
19
19
|
countAvailable: (labels: string[]) => number;
|
|
20
20
|
/**
|
|
21
21
|
* Agents this backend spawned that match the label set and have never run a
|
|
22
|
-
* job. `registeredAt`
|
|
22
|
+
* job. `registeredAt` stands in for "idle since". That is exact for a
|
|
23
|
+
* container-backend agent, which exits after its one job. An agent of another
|
|
24
|
+
* backend can finish a job and wait out its idle window before it exits, so it
|
|
25
|
+
* can read as idle for longer than it has been.
|
|
23
26
|
*
|
|
24
27
|
* Scoped to the backend because the reaper destroys what it is given; a
|
|
25
28
|
* matching static agent is ready capacity, not this pool's to tear down.
|