@kici-dev/orchestrator 0.6.1 → 0.8.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/dispatcher.d.ts +10 -1
- package/dist/agent/host-roster.d.ts +8 -3
- package/dist/agent/registry.d.ts +60 -5
- package/dist/app.d.ts +10 -5
- package/dist/cache/dep-cache.d.ts +14 -6
- package/dist/cache/global-eval-round-cache.d.ts +1 -1
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/source-cache.d.ts +92 -15
- package/dist/cancel/cancel-run.d.ts +20 -2
- package/dist/cancel/sweep-stuck-cancelling.d.ts +30 -0
- package/dist/cli/api-client.d.ts +10 -0
- package/dist/cli/commands/agent-service/logs.d.ts +1 -1
- package/dist/cli/commands/agent-service/restart.d.ts +1 -1
- package/dist/cli/commands/agent-service/start.d.ts +1 -1
- package/dist/cli/commands/agent-service/status.d.ts +1 -1
- package/dist/cli/commands/agent-service/stop.d.ts +1 -1
- package/dist/cli/commands/agent-service/uninstall.d.ts +1 -1
- package/dist/cli/commands/agent.d.ts +25 -0
- package/dist/cli/commands/config.d.ts +16 -0
- package/dist/cli/commands/db-backup.d.ts +46 -0
- package/dist/cli/commands/held-run.d.ts +9 -0
- package/dist/cli/commands/orchestrator-service/drain.d.ts +2 -19
- package/dist/cli/commands/orchestrator-service/index.d.ts +9 -1
- package/dist/cli/commands/orchestrator-service/install-env.d.ts +43 -2
- package/dist/cli/commands/orchestrator-service/logs.d.ts +1 -1
- package/dist/cli/commands/orchestrator-service/restart.d.ts +1 -1
- package/dist/cli/commands/orchestrator-service/start.d.ts +1 -1
- package/dist/cli/commands/orchestrator-service/status.d.ts +17 -1
- package/dist/cli/commands/orchestrator-service/stop.d.ts +1 -1
- package/dist/cli/commands/orchestrator-service/uninstall.d.ts +1 -1
- package/dist/cli/commands/orchestrator-service/upgrade.d.ts +2 -1
- package/dist/cli/commands/rotate.d.ts +1 -1
- package/dist/cli/commands/shared/env-file-mode.d.ts +16 -0
- package/dist/cli/commands/shared/upgrade-hooks.d.ts +32 -0
- package/dist/cli/commands/shared/upgrade-safety.d.ts +81 -0
- package/dist/cli/commands/shared/versioned-upgrade.d.ts +67 -12
- package/dist/cli/commands/source.d.ts +1 -0
- package/dist/cli/commands/token.d.ts +4 -0
- package/dist/cli/commands/trust-policy.d.ts +57 -1
- package/dist/cli/join.d.ts +1 -1
- package/dist/cli/service/backup-timer.d.ts +108 -0
- package/dist/cli/service/compose-path.d.ts +11 -0
- package/dist/cli/service/compose.d.ts +22 -0
- package/dist/cli/service/deploy-env.d.ts +10 -11
- package/dist/cli/service/env-assignment.d.ts +24 -0
- package/dist/cli/service/index.d.ts +2 -2
- package/dist/cli/service/instance/resolve.d.ts +71 -7
- package/dist/cli/service/instance/types.d.ts +15 -0
- package/dist/cli/service/launchd.d.ts +7 -0
- package/dist/cli/service/systemd.d.ts +1 -0
- package/dist/cli/service/types.d.ts +36 -0
- package/dist/cli/service/windows.d.ts +16 -0
- package/dist/cli/wizard/orchestrator-wizard.d.ts +12 -3
- package/dist/cli/wizard/prompts.d.ts +7 -0
- package/dist/cli.js +12544 -9299
- package/dist/cluster/cluster-settings-reader.d.ts +2 -2
- package/dist/cluster/coordinator.d.ts +11 -0
- package/dist/cluster/instance-heartbeat.d.ts +96 -0
- package/dist/cluster/join-client.d.ts +55 -5
- package/dist/cluster/join-handler.d.ts +1 -0
- package/dist/cluster/orphan-recovery.d.ts +16 -0
- package/dist/cluster/peer-registry.d.ts +10 -0
- package/dist/concurrency/dispatch-next-queued.d.ts +1 -13
- package/dist/concurrency/index.d.ts +1 -2
- package/dist/concurrency/queue-manager.d.ts +63 -18
- package/dist/config/env-overlay.d.ts +3 -2
- package/dist/config.d.ts +24 -0
- package/dist/contexts/release-queued-holds.d.ts +23 -1
- package/dist/dashboard/attestation-filters.d.ts +2 -0
- package/dist/dashboard/handler.d.ts +22 -2
- package/dist/db/deferred-indexes.d.ts +72 -0
- package/dist/db/migration-test-harness.d.ts +22 -0
- package/dist/db/migrations/131_execution_jobs_git_credentials.d.ts +30 -0
- package/dist/db/migrations/132_execution_runs_trigger_event.d.ts +23 -0
- package/dist/db/migrations/133_dispatch_queue_source_tar_digest.d.ts +23 -0
- package/dist/db/migrations/134_dispatch_queue_ownership.d.ts +4 -0
- package/dist/db/migrations/135_cluster_settings_retention_days.d.ts +4 -0
- package/dist/db/migrations/136_master_key_wrapped_tables_key_version.d.ts +4 -0
- package/dist/db/migrations/137_execution_runs_pr_head_context.d.ts +4 -0
- package/dist/db/migrations/138_execution_runs_subject_trigger_event.d.ts +4 -0
- package/dist/db/migrations/139_admin_token_subject.d.ts +4 -0
- package/dist/db/migrations/140_dispatch_queue_canonical_labels.d.ts +54 -0
- package/dist/db/migrations/141_event_log_shed.d.ts +15 -0
- package/dist/db/migrator.d.ts +47 -0
- package/dist/db/notify-listener.d.ts +60 -0
- package/dist/db/synthetic-job-ids.d.ts +24 -0
- package/dist/db/types.d.ts +169 -4
- package/dist/deployment/deployment-identity.d.ts +31 -1
- package/dist/diagnostics/checks/index.d.ts +2 -1
- package/dist/diagnostics/checks/retention.d.ts +24 -0
- package/dist/diagnostics/types.d.ts +7 -0
- package/dist/events/event-router.d.ts +1 -1
- package/dist/fault-injection-types.d.ts +53 -0
- package/dist/firecracker/host-network.d.ts +63 -8
- package/dist/git/credential-broker.d.ts +35 -2
- package/dist/git/inherited-secret.d.ts +7 -2
- package/dist/git/job-context.d.ts +7 -4
- package/dist/helpers/secure-write.d.ts +35 -0
- package/dist/index.js +196 -67
- package/dist/metrics/prometheus.d.ts +116 -1
- package/dist/oidc/db-signer.d.ts +27 -5
- package/dist/oidc/id-token-claims.d.ts +33 -2
- package/dist/oidc/oidc-mint-registration.d.ts +6 -0
- package/dist/oidc/orchestrator-mint.d.ts +8 -0
- package/dist/oidc/reconcile-signing-key.d.ts +37 -0
- package/dist/orchestrator-core.d.ts +9 -3
- package/dist/pipeline/decorating-secret-resolver.d.ts +1 -1
- package/dist/pipeline/dispatch-matched-workflow.d.ts +65 -14
- package/dist/pipeline/manual-schedule.d.ts +1 -0
- package/dist/pipeline/process-webhook.d.ts +20 -2
- package/dist/pipeline/route-or-dispatch-jobs.d.ts +7 -0
- package/dist/pipeline/run-event-context.d.ts +39 -0
- package/dist/policy/dashboard-write-policy-listener.d.ts +1 -1
- package/dist/policy/dashboard-write-policy.d.ts +47 -1
- package/dist/provenance/dispatch-context.d.ts +46 -0
- package/dist/provenance/verify-deferred-capture.d.ts +39 -0
- package/dist/providers/github/auth.d.ts +9 -0
- package/dist/providers/github/deliveries.d.ts +118 -0
- package/dist/providers/local/lock-file-fetcher.d.ts +8 -0
- package/dist/providers/universal-git/config.d.ts +1 -1
- package/dist/queue/cleanup.d.ts +13 -0
- package/dist/queue/job-queue.d.ts +162 -14
- package/dist/queue/retention.d.ts +105 -0
- package/dist/reporting/execution-tracker.d.ts +87 -36
- package/dist/routes/admin-db.d.ts +5 -0
- package/dist/routes/admin-events.d.ts +13 -0
- package/dist/routes/admin-held-runs.d.ts +11 -1
- package/dist/routes/admin-org-settings.d.ts +9 -0
- package/dist/routes/admin-runs.d.ts +7 -4
- package/dist/routes/admin-sources.d.ts +15 -0
- package/dist/routes/admin-trust-policy.d.ts +11 -0
- package/dist/routes/admin.d.ts +25 -0
- package/dist/scaler/backend-factory.d.ts +16 -0
- package/dist/scaler/bare-metal-backend.d.ts +22 -4
- package/dist/scaler/claim-store.d.ts +0 -2
- package/dist/scaler/config.d.ts +6 -3
- package/dist/scaler/container-backend.d.ts +102 -3
- package/dist/scaler/container-hostconfig.d.ts +60 -0
- package/dist/scaler/event-backend.d.ts +7 -6
- package/dist/scaler/file-tail.d.ts +25 -2
- package/dist/scaler/firecracker-api.d.ts +9 -1
- package/dist/scaler/firecracker-backend.d.ts +135 -2
- package/dist/scaler/host-access.d.ts +59 -0
- package/dist/scaler/index.d.ts +1 -0
- package/dist/scaler/manager.d.ts +51 -2
- package/dist/scaler/scaler-state-store.d.ts +13 -0
- package/dist/scaler/types.d.ts +15 -7
- package/dist/scaler/warm-pool.d.ts +6 -4
- package/dist/secrets/config.d.ts +48 -0
- package/dist/secrets/dashboard-encryption-key.d.ts +19 -0
- package/dist/secrets/ephemeral-keys.d.ts +9 -3
- package/dist/secrets/index.d.ts +3 -1
- package/dist/secrets/job-secret-gate.d.ts +76 -0
- package/dist/secrets/master-key-rotation.d.ts +65 -0
- package/dist/secrets/secret-output-crypto.d.ts +17 -0
- package/dist/secrets/secret-resolver.d.ts +23 -6
- package/dist/secrets/source-credentials.d.ts +1 -1
- package/dist/secrets/token-manager.d.ts +4 -1
- package/dist/security/identity-link.d.ts +1 -1
- package/dist/security/reduced-privilege-note.d.ts +5 -0
- package/dist/security/trust-directory-store.d.ts +1 -1
- package/dist/security/trust-policy-gate.d.ts +70 -6
- package/dist/security/trust-tier.d.ts +31 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +10793 -5323
- package/dist/sources/source-manager.d.ts +1 -1
- package/dist/stale-detector/stale-dispatch-candidates.d.ts +43 -0
- package/dist/standalone.js +9385 -4160
- package/dist/storage/loopback-guard.d.ts +53 -6
- package/dist/webhook/event-log.d.ts +14 -1
- package/dist/webhook/generic-sources-listener.d.ts +1 -1
- package/dist/webhook/ingest-admission.d.ts +24 -0
- package/dist/webhook/ingest-overflow-replayer.d.ts +78 -14
- package/dist/webhook/ingest-overflow-types.d.ts +13 -1
- package/dist/webhook/register-source-bundle.d.ts +23 -0
- package/dist/webhook/relay-reinject.d.ts +11 -12
- package/dist/webhook/shed-breadcrumb.d.ts +15 -0
- package/dist/worker/in-memory-job-queue.d.ts +9 -0
- package/dist/ws/fleet-agent-collector.d.ts +7 -0
- package/dist/ws/git-credential-relay.d.ts +41 -12
- package/dist/ws/platform-client.d.ts +28 -5
- package/dist/ws/test-relay-handlers.d.ts +2 -0
- package/installer-image-digests.json +3 -3
- package/package.json +4 -4
- package/sbom.spdx.json +53 -53
- package/dist/concurrency/group-tracker.d.ts +0 -51
- package/dist/scaler/nftables.d.ts +0 -111
|
@@ -6,9 +6,14 @@
|
|
|
6
6
|
* Handles full container lifecycle: pull, create, start, stop, remove.
|
|
7
7
|
* Supports container socket sharing, resource limits, and orphan cleanup.
|
|
8
8
|
*/
|
|
9
|
+
import Docker from 'dockerode';
|
|
9
10
|
import { type ToolRequirement } from '@kici-dev/shared';
|
|
10
11
|
import type { AgentTokenStore } from '../agent/token-store.js';
|
|
11
12
|
import type { ScalerBackend, ScalerDestroyContext, ManagedAgent, LabelSetConfig, LogCapture, ResourceRequest, EffectiveLimits, SpawnContext, ScalerEventCallback, ValidationResult, ScalerEntry } from './types.js';
|
|
13
|
+
/** Name of the isolated bridge network for agent containers. */
|
|
14
|
+
export declare const ISOLATED_NETWORK_NAME = "kici-agent-net";
|
|
15
|
+
/** Gateway IP for the isolated agent network (host-side). */
|
|
16
|
+
export declare const ISOLATED_NETWORK_GATEWAY = "172.30.0.1";
|
|
12
17
|
/**
|
|
13
18
|
* Result of runtime detection.
|
|
14
19
|
*/
|
|
@@ -40,6 +45,12 @@ export interface ContainerScalerBackendOptions {
|
|
|
40
45
|
extraHosts?: string[];
|
|
41
46
|
/** Disable nftables-based network isolation (default: true). Set to false when nft is unavailable (e.g. rootless containers without NET_ADMIN). */
|
|
42
47
|
networkIsolation?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Extra `hostAccess` entries the orchestrator itself directed agents at —
|
|
50
|
+
* today its object-storage endpoint. Folded into the agent default so the
|
|
51
|
+
* narrowing does not cut off a host-local object store.
|
|
52
|
+
*/
|
|
53
|
+
hostServices?: string[];
|
|
43
54
|
/** Token store for creating ephemeral agent auth tokens. Optional -- when undefined, no token is injected. */
|
|
44
55
|
tokenStore?: AgentTokenStore;
|
|
45
56
|
/** TTL for ephemeral agent tokens in ms. Default: 1 hour. */
|
|
@@ -52,7 +63,27 @@ export interface ContainerScalerBackendOptions {
|
|
|
52
63
|
tokenTtlProvider?: () => Promise<number>;
|
|
53
64
|
/** Agent roles for this scaler. undefined = all, [] = execution only. */
|
|
54
65
|
roles?: string[];
|
|
66
|
+
/**
|
|
67
|
+
* Whether an agent id is currently registered with this orchestrator.
|
|
68
|
+
*
|
|
69
|
+
* Used by `cleanupOrphans` to distinguish a running container whose agent is
|
|
70
|
+
* doing work from one whose agent never registered (or has gone). Defaults
|
|
71
|
+
* to "assume registered" when unwired, so an un-plumbed caller never reaps a
|
|
72
|
+
* live agent.
|
|
73
|
+
*/
|
|
74
|
+
isRegistered?: (agentId: string) => boolean;
|
|
55
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Create or find the isolated agent bridge network, and return its id.
|
|
78
|
+
*
|
|
79
|
+
* Module-level rather than a method because the bare-metal backend's container
|
|
80
|
+
* mode attaches its agent containers to the same network: both agent-container
|
|
81
|
+
* spawn paths must land on it, or one of them runs on the runtime's default
|
|
82
|
+
* bridge with full LAN, RFC1918 and cloud-metadata reach.
|
|
83
|
+
*
|
|
84
|
+
* Idempotent, including against a concurrent creator (409).
|
|
85
|
+
*/
|
|
86
|
+
export declare function ensureIsolatedNetwork(docker: Docker): Promise<string>;
|
|
56
87
|
export declare class ContainerScalerBackend implements ScalerBackend {
|
|
57
88
|
readonly type: "container";
|
|
58
89
|
readonly spawnsOnLocalHost: boolean;
|
|
@@ -69,6 +100,8 @@ export declare class ContainerScalerBackend implements ScalerBackend {
|
|
|
69
100
|
private readonly extraHosts?;
|
|
70
101
|
/** Whether nftables-based network isolation is enabled */
|
|
71
102
|
private readonly networkIsolation;
|
|
103
|
+
/** Host services the orchestrator directed agents at, as `hostAccess` entries. */
|
|
104
|
+
private readonly hostServices?;
|
|
72
105
|
/** Token store for creating ephemeral agent auth tokens */
|
|
73
106
|
private readonly tokenStore?;
|
|
74
107
|
/** TTL for ephemeral agent tokens in ms */
|
|
@@ -77,6 +110,8 @@ export declare class ContainerScalerBackend implements ScalerBackend {
|
|
|
77
110
|
private readonly tokenTtlProvider?;
|
|
78
111
|
/** Agent roles for this scaler. undefined = all, [] = execution only. */
|
|
79
112
|
private readonly roles;
|
|
113
|
+
/** Registration probe for the orphan sweep; fail-safe default assumes registered. */
|
|
114
|
+
private readonly isRegistered;
|
|
80
115
|
/** ID of the isolated bridge network (set after creation/discovery) */
|
|
81
116
|
private isolatedNetworkId;
|
|
82
117
|
/** Host bridge interface name for the isolated network (used by nftables) */
|
|
@@ -132,6 +167,15 @@ export declare class ContainerScalerBackend implements ScalerBackend {
|
|
|
132
167
|
* `spawnContext` rather than letting the agent claim them.
|
|
133
168
|
*/
|
|
134
169
|
private mintAgentIdentity;
|
|
170
|
+
/**
|
|
171
|
+
* Write one agent container's nftables rules, on both hooks.
|
|
172
|
+
*
|
|
173
|
+
* `forward` governs what the container reaches THROUGH the host; `input`
|
|
174
|
+
* governs what it reaches ON the host. A packet to one of the host's own
|
|
175
|
+
* addresses is delivered on input and never traverses forward, so the two
|
|
176
|
+
* are not interchangeable and both are written here.
|
|
177
|
+
*/
|
|
178
|
+
private applyIsolationRules;
|
|
135
179
|
spawn(labelSet: string[], agentId: string, orchestratorUrl: string, onEvent?: ScalerEventCallback, effectiveLimits?: EffectiveLimits, spawnContext?: SpawnContext, signal?: AbortSignal): Promise<ManagedAgent>;
|
|
136
180
|
getScalerContext(agentId: string): Record<string, unknown> | undefined;
|
|
137
181
|
destroy(managedId: string, _context?: ScalerDestroyContext): Promise<void>;
|
|
@@ -145,10 +189,65 @@ export declare class ContainerScalerBackend implements ScalerBackend {
|
|
|
145
189
|
entry?: ScalerEntry;
|
|
146
190
|
}): ValidationResult;
|
|
147
191
|
/**
|
|
148
|
-
* Clean up orphaned
|
|
149
|
-
*
|
|
150
|
-
*
|
|
192
|
+
* Clean up this scaler's orphaned containers and stale isolation rules.
|
|
193
|
+
*
|
|
194
|
+
* Every filter below exists because the sweep used to list every
|
|
195
|
+
* `kici-managed=true` container on the host and force-remove it, running at
|
|
196
|
+
* orchestrator boot, at worker-peer start, on a live config reload that adds
|
|
197
|
+
* a scaler, and from `kici-admin scaler reap-orphans`. Two orchestrators
|
|
198
|
+
* sharing one docker host therefore killed each other's running agents on
|
|
199
|
+
* every boot, and adding a second container scaler removed the first one's
|
|
200
|
+
* agents mid-job — every in-flight job failing as "agent disconnected" on a
|
|
201
|
+
* healthy system.
|
|
202
|
+
*
|
|
203
|
+
* A container is removed only when all of these hold:
|
|
204
|
+
* - it carries this scaler's own `kici-scaler-name`, so a backend can only
|
|
205
|
+
* ever reap what it stamped;
|
|
206
|
+
* - its `kici-agent-id` is not one this backend is currently tracking
|
|
207
|
+
* (spawning, running, or destroying);
|
|
208
|
+
* - it is not running, or it is running with an agent id that is not
|
|
209
|
+
* registered — a running, registered agent is doing work.
|
|
210
|
+
*
|
|
211
|
+
* Returns the count of cleaned containers plus reaped rules.
|
|
151
212
|
*/
|
|
152
213
|
cleanupOrphans(): Promise<number>;
|
|
214
|
+
/**
|
|
215
|
+
* Remove containers this backend spawned for one agent id.
|
|
216
|
+
*
|
|
217
|
+
* Implements the optional `ScalerBackend.reapUnowned` hook. It is what
|
|
218
|
+
* reclaims a container whose registration the orchestrator refused — the
|
|
219
|
+
* case `cleanupOrphans` used to cover by removing every `kici-managed`
|
|
220
|
+
* container on the host, which is exactly the breadth that killed other
|
|
221
|
+
* orchestrators' live agents.
|
|
222
|
+
*
|
|
223
|
+
* @returns true when a container was found and removed
|
|
224
|
+
*/
|
|
225
|
+
reapUnowned(managedId: string): Promise<boolean>;
|
|
226
|
+
/**
|
|
227
|
+
* Delete isolation rules for container IPs on this scaler's isolated network
|
|
228
|
+
* that no tracked container holds.
|
|
229
|
+
*
|
|
230
|
+
* Rules are removed only on the teardown paths this process drives, so a
|
|
231
|
+
* crash or a `kill -9` strands them. The network's address pool is recycled,
|
|
232
|
+
* so the next container to take that IP inherits the dead job's allowlist.
|
|
233
|
+
*
|
|
234
|
+
* Scoped to addresses no live container holds: `kici-agent-net` is one fixed
|
|
235
|
+
* network shared by every container-spawning backend on the host, so this
|
|
236
|
+
* backend's own tracking map is not enough to tell an abandoned address from
|
|
237
|
+
* a neighbour's live one.
|
|
238
|
+
*/
|
|
239
|
+
private reapUnownedIsolationRules;
|
|
240
|
+
/**
|
|
241
|
+
* Every address held on the isolated agent network by a kici-managed
|
|
242
|
+
* container, across every scaler and every orchestrator on this runtime.
|
|
243
|
+
*
|
|
244
|
+
* Deliberately NOT filtered by `kici-scaler-name`: the point is to see the
|
|
245
|
+
* neighbours, and a neighbour is by definition stamped with a name this
|
|
246
|
+
* backend does not know.
|
|
247
|
+
*
|
|
248
|
+
* @returns the addresses, or `null` when the runtime could not be listed — in
|
|
249
|
+
* which case the caller reaps nothing rather than reaping blind.
|
|
250
|
+
*/
|
|
251
|
+
private isolatedNetworkAddressesInUse;
|
|
153
252
|
}
|
|
154
253
|
//# sourceMappingURL=container-backend.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `HostConfig` every KiCI agent container is created with.
|
|
3
|
+
*
|
|
4
|
+
* Two backends spawn an agent container — `ContainerScalerBackend` and the
|
|
5
|
+
* bare-metal backend's container mode — and they had diverged into two
|
|
6
|
+
* postures for one threat: the first applied resource limits, the second
|
|
7
|
+
* applied none and dropped the limits the operator configured on the floor.
|
|
8
|
+
* One builder is what keeps them from diverging again the next time either is
|
|
9
|
+
* touched.
|
|
10
|
+
*/
|
|
11
|
+
import type Docker from 'dockerode';
|
|
12
|
+
import type { EffectiveLimits } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Maximum processes an agent container may create.
|
|
15
|
+
*
|
|
16
|
+
* High enough that no realistic build reaches it, low enough to bound a fork
|
|
17
|
+
* bomb. Not operator-configurable: a `limits.pids` key is a new surface with
|
|
18
|
+
* its own docs and validation, and nothing yet says the constant is wrong.
|
|
19
|
+
*/
|
|
20
|
+
export declare const AGENT_CONTAINER_PIDS_LIMIT = 4096;
|
|
21
|
+
/**
|
|
22
|
+
* Why this container is NOT capability-dropped, though the job containers the
|
|
23
|
+
* agent starts are.
|
|
24
|
+
*
|
|
25
|
+
* Both spawn paths set `KICI_EXECUTION_MODE=bare-metal`, and bare-metal
|
|
26
|
+
* container mode also sets `KICI_JOB_IMAGE_AGENT=1` — so unless the job
|
|
27
|
+
* declares its own image, the workflow's steps run as host processes INSIDE
|
|
28
|
+
* this container. It is a build execution environment, not a supervisor.
|
|
29
|
+
* `CapDrop: ['ALL']` here would therefore land on customer build steps:
|
|
30
|
+
* `apt-get` and `dpkg` need CHOWN, DAC_OVERRIDE, FOWNER, SETUID and SETGID,
|
|
31
|
+
* and `no-new-privileges` breaks `sudo` and every setuid binary. There is no
|
|
32
|
+
* opt-out, so the break would be silent and total for `type: container`
|
|
33
|
+
* scalers.
|
|
34
|
+
*
|
|
35
|
+
* The hardening the operator docs promise already exists, one boundary in:
|
|
36
|
+
* the agent applies `CapDrop: ['ALL']`, `no-new-privileges` and a tighter
|
|
37
|
+
* `PidsLimit` to each nested JOB container
|
|
38
|
+
* (`packages/agent/src/execution/sandbox/container-hardening.ts`). That is the
|
|
39
|
+
* boundary that confines customer code; this one hosts it.
|
|
40
|
+
*
|
|
41
|
+
* `PidsLimit` is the exception and stays: a fork-bomb ceiling costs a build
|
|
42
|
+
* nothing at 4096.
|
|
43
|
+
*/
|
|
44
|
+
export interface AgentContainerHostConfigInput {
|
|
45
|
+
/** Resolved cpu / memory ceiling for this spawn, when the operator set one. */
|
|
46
|
+
limits?: EffectiveLimits;
|
|
47
|
+
/** Volume and path bind mounts. */
|
|
48
|
+
binds?: string[];
|
|
49
|
+
/** Extra `host:ip` entries (e.g. a private registry alias). */
|
|
50
|
+
extraHosts?: string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the agent container's `HostConfig`.
|
|
54
|
+
*
|
|
55
|
+
* Carries the operator's resource limits, a fork-bomb ceiling, and nothing
|
|
56
|
+
* that would restrict the build steps this container runs — see the note on
|
|
57
|
+
* {@link AGENT_CONTAINER_PIDS_LIMIT}.
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildAgentContainerHostConfig(input: AgentContainerHostConfigInput): Docker.HostConfig;
|
|
60
|
+
//# sourceMappingURL=container-hostconfig.d.ts.map
|
|
@@ -108,12 +108,13 @@ export declare class EventScalerBackend implements ScalerBackend {
|
|
|
108
108
|
*/
|
|
109
109
|
shutdownAll(): Promise<void>;
|
|
110
110
|
/**
|
|
111
|
-
* Apply the new config. The backend reads `roles`, `
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
111
|
+
* Apply the new config. The backend reads `roles`, `agentTokenTtlSeconds`
|
|
112
|
+
* and `provisioningTargets` off its entry at every spawn (and
|
|
113
|
+
* `mandatoryLabels` only when a spawn reaches it without a context), so the
|
|
114
|
+
* entry is replaced here — otherwise a reload that retargets the
|
|
115
|
+
* provisioning workflow, or changes the scaler's roles, would keep emitting
|
|
116
|
+
* scale-up events to the old workflow refs and minting agent labels from the
|
|
117
|
+
* old roles, while the manager's own routing gate used the new ones.
|
|
117
118
|
*/
|
|
118
119
|
reload(labelSets: LabelSetConfig[], opts?: {
|
|
119
120
|
maxAgents?: number;
|
|
@@ -2,12 +2,35 @@
|
|
|
2
2
|
* File tailing utility for append-only log files.
|
|
3
3
|
*
|
|
4
4
|
* Provides an async generator that yields complete lines as they are appended
|
|
5
|
-
* to a file, using `fs.
|
|
5
|
+
* to a file, using `fs.watchFile()` for change notifications and partial line
|
|
6
6
|
* buffering for split writes.
|
|
7
7
|
*
|
|
8
8
|
* Used by the Firecracker backend to tail serial console and VMM log files
|
|
9
|
-
* from the jailer chroot directory.
|
|
9
|
+
* from the jailer chroot directory. The guest writes to `/dev/console`
|
|
10
|
+
* unprivileged, so everything this module buffers is tenant-controlled: every
|
|
11
|
+
* buffer here is bounded, and an overflow is reported in-band rather than
|
|
12
|
+
* growing the orchestrator's heap.
|
|
10
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* Largest logical line kept intact. A guest that writes without ever emitting a
|
|
16
|
+
* newline (`yes | tr -d '\n' > /dev/console`) would otherwise grow the partial
|
|
17
|
+
* buffer at disk-write speed until the Node heap limit takes the orchestrator —
|
|
18
|
+
* and every in-flight job on it — down.
|
|
19
|
+
*/
|
|
20
|
+
export declare const MAX_LINE_BYTES: number;
|
|
21
|
+
/**
|
|
22
|
+
* Largest slice read from the file per poll. A burst is drained across several
|
|
23
|
+
* ticks instead of materialising the whole delta in one string.
|
|
24
|
+
*/
|
|
25
|
+
export declare const MAX_READ_BYTES: number;
|
|
26
|
+
/**
|
|
27
|
+
* Largest number of complete lines held for the consumer. `forwardLine` →
|
|
28
|
+
* Winston → Loki is orders of magnitude slower than a guest can write to the
|
|
29
|
+
* serial console, so the queue is the second place unbounded growth appears.
|
|
30
|
+
* Matches the agent-side log buffer's cap, which uses gap markers for the same
|
|
31
|
+
* situation.
|
|
32
|
+
*/
|
|
33
|
+
export declare const MAX_QUEUED_LINES = 10000;
|
|
11
34
|
/**
|
|
12
35
|
* Tail an append-only file, yielding complete lines as they are written.
|
|
13
36
|
*
|
|
@@ -35,7 +35,15 @@ export declare class FirecrackerApiError extends Error {
|
|
|
35
35
|
*/
|
|
36
36
|
export declare class FirecrackerApi {
|
|
37
37
|
private readonly socketPath;
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Aborts every in-flight and subsequent request on this client.
|
|
40
|
+
*
|
|
41
|
+
* Set by the spawn path so a spawn deadline actually stops the VM
|
|
42
|
+
* provisioning calls, rather than leaving them running against a VM the
|
|
43
|
+
* manager has already given up on.
|
|
44
|
+
*/
|
|
45
|
+
private signal;
|
|
46
|
+
constructor(socketPath: string, signal?: AbortSignal);
|
|
39
47
|
/**
|
|
40
48
|
* Send an HTTP request to the Firecracker API.
|
|
41
49
|
*
|
|
@@ -34,6 +34,14 @@ export interface FirecrackerManagedAgent extends ManagedAgent {
|
|
|
34
34
|
ip: string;
|
|
35
35
|
/** TAP device name for cleanup */
|
|
36
36
|
tapDevice: string;
|
|
37
|
+
/**
|
|
38
|
+
* The `hostAccess` policy resolved for this VM at provision time.
|
|
39
|
+
*
|
|
40
|
+
* Carried on the record because a bridge self-heal re-applies isolation for
|
|
41
|
+
* every live VM, and that path has no `orchestratorUrl` to re-derive the
|
|
42
|
+
* default from.
|
|
43
|
+
*/
|
|
44
|
+
hostAccess?: string[];
|
|
37
45
|
}
|
|
38
46
|
export interface FirecrackerScalerBackendOptions {
|
|
39
47
|
/** Human-readable name for this scaler */
|
|
@@ -44,6 +52,12 @@ export interface FirecrackerScalerBackendOptions {
|
|
|
44
52
|
maxAgents: number;
|
|
45
53
|
/** Shared IP allocator instance */
|
|
46
54
|
ipAllocator: IpAllocator;
|
|
55
|
+
/**
|
|
56
|
+
* Extra `hostAccess` entries the orchestrator itself directed agents at —
|
|
57
|
+
* today its object-storage endpoint. Folded into the agent default so the
|
|
58
|
+
* narrowing does not cut off a host-local object store.
|
|
59
|
+
*/
|
|
60
|
+
hostServices?: string[];
|
|
47
61
|
/** Path to firecracker binary */
|
|
48
62
|
firecrackerPath: string;
|
|
49
63
|
/** Path to jailer binary */
|
|
@@ -108,6 +122,15 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
|
|
|
108
122
|
readonly logsSource = "firecracker-serial";
|
|
109
123
|
/** AbortControllers for file tailing per managed VM (keyed by agent ID) */
|
|
110
124
|
private readonly tailAbortControllers;
|
|
125
|
+
/**
|
|
126
|
+
* Per-spawn `scaler.failed` emitters, keyed by agent id.
|
|
127
|
+
*
|
|
128
|
+
* The jailer's `exit` handler and the spawn-deadline abort both fire outside
|
|
129
|
+
* `spawn`'s own scope, and the manager releases a spawn reservation only on a
|
|
130
|
+
* failure event. Holding the emitter here is what lets those two paths report
|
|
131
|
+
* the failure instead of leaving the reservation held until restart.
|
|
132
|
+
*/
|
|
133
|
+
private readonly spawnFailureHandlers;
|
|
111
134
|
private _labelSets;
|
|
112
135
|
private readonly name;
|
|
113
136
|
private readonly ipAllocator;
|
|
@@ -122,6 +145,8 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
|
|
|
122
145
|
private readonly bridgeName;
|
|
123
146
|
private readonly cidr;
|
|
124
147
|
private readonly gateway;
|
|
148
|
+
/** Host services the orchestrator directed agents at, as `hostAccess` entries. */
|
|
149
|
+
private readonly hostServices?;
|
|
125
150
|
private readonly netmask;
|
|
126
151
|
private readonly table;
|
|
127
152
|
private readonly tokenStore?;
|
|
@@ -145,7 +170,16 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
|
|
|
145
170
|
static getRequiredTools(entry: ScalerEntry): ToolRequirement[];
|
|
146
171
|
get labelSets(): LabelSetConfig[];
|
|
147
172
|
getActiveCount(): number;
|
|
148
|
-
spawn(labelSet: string[], agentId: string, orchestratorUrl: string, onEvent?: ScalerEventCallback, effectiveLimits?: EffectiveLimits, spawnContext?: SpawnContext,
|
|
173
|
+
spawn(labelSet: string[], agentId: string, orchestratorUrl: string, onEvent?: ScalerEventCallback, effectiveLimits?: EffectiveLimits, spawnContext?: SpawnContext, signal?: AbortSignal): Promise<ManagedAgent>;
|
|
174
|
+
/**
|
|
175
|
+
* Tear down a spawn that will never produce a usable VM, from a path that is
|
|
176
|
+
* not `spawn`'s own `catch` — a deadline abort, or the jailer exiting during
|
|
177
|
+
* boot.
|
|
178
|
+
*
|
|
179
|
+
* Idempotent: whichever of the two fires first wins and the other is a no-op,
|
|
180
|
+
* because `cleanupFailedSpawn` removes the tracking entry.
|
|
181
|
+
*/
|
|
182
|
+
private abandonSpawn;
|
|
149
183
|
getScalerContext(agentId: string): Record<string, unknown> | undefined;
|
|
150
184
|
/**
|
|
151
185
|
* Bridge config for host-network diagnostics (read-only snapshot).
|
|
@@ -164,7 +198,41 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
|
|
|
164
198
|
* No-op unless autoProvisionHost is set. Throws on a real provision failure;
|
|
165
199
|
* ScalerManager.ensureHostsReady catches per-backend and degrades this scaler.
|
|
166
200
|
*/
|
|
201
|
+
/**
|
|
202
|
+
* nft options for this backend's own rules.
|
|
203
|
+
*
|
|
204
|
+
* `table` is the operator-configured one, not the literal `kici`: with two
|
|
205
|
+
* coordinators on one host, coordinator B's baseline goes to `kici_b` while
|
|
206
|
+
* its per-VM rules used to go to `kici` — coordinator A's table. A's next
|
|
207
|
+
* re-provision then swept B's live VMs' isolation along with its own, and B's
|
|
208
|
+
* rules were evaluated against A's subnet-scoped baseline.
|
|
209
|
+
*/
|
|
210
|
+
private nftOpts;
|
|
167
211
|
ensureHostReady(): Promise<void>;
|
|
212
|
+
/**
|
|
213
|
+
* Fail loudly on a kernel that will not honour bridge port isolation.
|
|
214
|
+
*
|
|
215
|
+
* Without the flag every concurrent tenant's VM can reach every other one at
|
|
216
|
+
* L2, and no nft rule sees that traffic — so the isolation the operator docs
|
|
217
|
+
* promise silently does not exist. The same fail-closed posture
|
|
218
|
+
* `validateNftablesAvailability` takes for a missing `nft`: better to refuse
|
|
219
|
+
* to run VMs than to run them unisolated.
|
|
220
|
+
*
|
|
221
|
+
* The probe is an end-to-end one — create a TAP, enslave it isolated, delete
|
|
222
|
+
* it — because the flag can be rejected by the kernel, by iproute2, or by the
|
|
223
|
+
* bridge itself, and only the real command exercises all three.
|
|
224
|
+
*/
|
|
225
|
+
private assertBridgePortIsolationSupported;
|
|
226
|
+
/**
|
|
227
|
+
* Re-apply per-VM isolation rules for every VM this backend still tracks.
|
|
228
|
+
*
|
|
229
|
+
* Provisioning no longer deletes the table, but it does sweep the `forward`
|
|
230
|
+
* chain of everything no live VM owns, and an old-shaped host carries its
|
|
231
|
+
* baseline there too. Re-applying is what guarantees a self-heal cannot leave
|
|
232
|
+
* a running VM fail-open. Each identifier is removed before it is re-added,
|
|
233
|
+
* so a self-heal that fires twice does not duplicate the rules.
|
|
234
|
+
*/
|
|
235
|
+
private reapplyIsolationForTrackedVms;
|
|
168
236
|
destroy(managedId: string, _context?: ScalerDestroyContext): Promise<void>;
|
|
169
237
|
/**
|
|
170
238
|
* Force-reclaim a VM this backend no longer tracks in memory.
|
|
@@ -222,6 +290,18 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
|
|
|
222
290
|
* Returns the count of cleaned orphans.
|
|
223
291
|
*/
|
|
224
292
|
cleanupOrphans(): Promise<number>;
|
|
293
|
+
/**
|
|
294
|
+
* Delete isolation rules for every identifier no live VM owns.
|
|
295
|
+
*
|
|
296
|
+
* The live set is the VM ids the passes above established, mapped to the IPs
|
|
297
|
+
* this scaler allocated for them. An identifier the DB does not attribute to
|
|
298
|
+
* a live VM of this scaler is left alone: a second scaler on the same host
|
|
299
|
+
* writes to the same chain, and reaping its rules would cut its running jobs
|
|
300
|
+
* off the network — the mistake this whole sweep exists to stop making.
|
|
301
|
+
*
|
|
302
|
+
* @returns the number of rules deleted
|
|
303
|
+
*/
|
|
304
|
+
private reapUnownedIsolationRules;
|
|
225
305
|
/**
|
|
226
306
|
* Default interval for periodic orphan sweeps (15 minutes).
|
|
227
307
|
*
|
|
@@ -309,7 +389,60 @@ export declare class FirecrackerScalerBackend implements ScalerBackend {
|
|
|
309
389
|
*/
|
|
310
390
|
getChrootDir(agentId: string): string;
|
|
311
391
|
/**
|
|
312
|
-
*
|
|
392
|
+
* Probe the process a VM's PID file names.
|
|
393
|
+
*
|
|
394
|
+
* Checks, in order:
|
|
395
|
+
* 1. the file parses to a plausible PID;
|
|
396
|
+
* 2. `/proc/<pid>/stat` exists at all — the file is world-readable
|
|
397
|
+
* regardless of the jailer's `--uid` drop, so an unreadable one means no
|
|
398
|
+
* such process;
|
|
399
|
+
* 3. field 22 (`starttime`) is not later than the PID file's mtime — a
|
|
400
|
+
* recycled number belongs to a process that started after the file
|
|
401
|
+
* naming it was written;
|
|
402
|
+
* 4. field 2 (`comm`) is `firecracker` (11 characters, so it is not
|
|
403
|
+
* truncated by the 15-character `comm` limit) and `/proc/<pid>/root` —
|
|
404
|
+
* where the jailer pivot_roots — is this VM's chroot when the link is
|
|
405
|
+
* readable at all. Those carry `identityConfirmed`, not liveness.
|
|
406
|
+
*/
|
|
407
|
+
private probeVmPid;
|
|
408
|
+
/**
|
|
409
|
+
* Read a VM's PID file and return the PID only when the process it names is
|
|
410
|
+
* provably still *this* VM's firecracker process.
|
|
411
|
+
*
|
|
412
|
+
* This is the guard for acting ON the process — `destroy` and `reapUnowned`
|
|
413
|
+
* SIGKILL through it — so it fails closed: anything short of a confirmed
|
|
414
|
+
* identity yields `undefined` and nothing is signalled.
|
|
415
|
+
*
|
|
416
|
+
* @returns the PID when it is provably this VM's, otherwise `undefined`
|
|
417
|
+
*/
|
|
418
|
+
private readVmPid;
|
|
419
|
+
/**
|
|
420
|
+
* Whether a chroot's PID file names a process that is running right now.
|
|
421
|
+
*
|
|
422
|
+
* This is the guard for the orphan reaper, which asks the opposite question
|
|
423
|
+
* from {@link readVmPid} and therefore fails closed in the opposite
|
|
424
|
+
* direction. `readVmPid` refuses to signal a PID it cannot claim; the reaper
|
|
425
|
+
* must refuse to DELETE THE ROOTFS under one. A process whose `comm` or
|
|
426
|
+
* `/proc/<pid>/root` does not match is still a process, and deleting the
|
|
427
|
+
* files it is running out of corrupts whatever it is — while sparing it costs
|
|
428
|
+
* only the disk one chroot occupies.
|
|
429
|
+
*
|
|
430
|
+
* The leak the identity checks exist to prevent is unaffected: a recycled
|
|
431
|
+
* number is detected by start time, which this guard honours, so a chroot
|
|
432
|
+
* whose VM is genuinely over is still reclaimed.
|
|
433
|
+
*/
|
|
434
|
+
private isChrootPidRunning;
|
|
435
|
+
/**
|
|
436
|
+
* Whether *this* VM's own firecracker process is still up.
|
|
437
|
+
*
|
|
438
|
+
* Liveness is `readVmPid` plus signal 0, so it answers the identity-bearing
|
|
439
|
+
* question — "is the VM I started still running?" — that
|
|
440
|
+
* {@link waitForProcessExit} needs. Without the identity checks a recycled
|
|
441
|
+
* PID answers signal 0 successfully and the VM is spared forever.
|
|
442
|
+
*
|
|
443
|
+
* The orphan reaper asks a different question and uses
|
|
444
|
+
* {@link isChrootPidRunning} instead: a PID it cannot claim is still a live
|
|
445
|
+
* process, and its rootfs must not be deleted underneath it.
|
|
313
446
|
*/
|
|
314
447
|
private isVmProcessAlive;
|
|
315
448
|
/**
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a scaled agent may reach on the orchestrator's own host.
|
|
3
|
+
*
|
|
4
|
+
* An agent container or Firecracker VM sends its packets to one of the host's
|
|
5
|
+
* addresses — a bridge gateway, the host's LAN address — and those are
|
|
6
|
+
* delivered on the netfilter input hook. The `forward` rules the backends
|
|
7
|
+
* already write never see them, so until the `input` chain existed a sandbox
|
|
8
|
+
* reached every port on the host. These helpers resolve the narrow set it
|
|
9
|
+
* actually needs, expressed in the same `hostAccess` vocabulary an operator
|
|
10
|
+
* writes in `scalers.yaml`.
|
|
11
|
+
*/
|
|
12
|
+
import type { NetworkPolicy } from '@kici-dev/shared/net';
|
|
13
|
+
import type { AppConfig } from '../config.js';
|
|
14
|
+
/**
|
|
15
|
+
* Host-access entries for the agent-facing storage endpoint, when it is a
|
|
16
|
+
* service on this host rather than a remote one.
|
|
17
|
+
*
|
|
18
|
+
* A scaled agent connects to storage directly — that is why
|
|
19
|
+
* `assertAgentReachableStorage` refuses to start on a loopback endpoint — so an
|
|
20
|
+
* orchestrator-port-only default would cut off every deployment running its own
|
|
21
|
+
* object store beside the orchestrator.
|
|
22
|
+
*
|
|
23
|
+
* Two shapes are recognised, and nothing else:
|
|
24
|
+
*
|
|
25
|
+
* - **An IP literal.** The entry is scoped to that exact address and port, the
|
|
26
|
+
* narrowest rule that can work.
|
|
27
|
+
* - **A name with an explicit port.** No address rule can be built for a name
|
|
28
|
+
* that may move, so the entry names the port on any host address. It is
|
|
29
|
+
* still one port rather than the whole host.
|
|
30
|
+
*
|
|
31
|
+
* A name with no port is a public endpoint (`https://…`), which the agent
|
|
32
|
+
* reaches through the host over the `forward` hook and which no input rule
|
|
33
|
+
* governs. Emitting `*:443` for it would open a host port for nothing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function storageHostAccessEntries(config: AppConfig): string[];
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the `hostAccess` policy for one agent sandbox.
|
|
38
|
+
*
|
|
39
|
+
* An explicit `hostAccess` on the label set wins outright: host reachability is
|
|
40
|
+
* operator-configurable policy, and an operator who names it has said exactly
|
|
41
|
+
* what this class of agent may reach.
|
|
42
|
+
*
|
|
43
|
+
* The default is the set an agent cannot work without:
|
|
44
|
+
*
|
|
45
|
+
* - **DNS on the bridge gateway.** Rootful podman resolves there, so a sandbox
|
|
46
|
+
* with no such rule cannot resolve the orchestrator's own hostname. Docker
|
|
47
|
+
* resolves inside the container's netns and is unaffected either way.
|
|
48
|
+
* - **The orchestrator's port**, on any host address, since the address the
|
|
49
|
+
* agent was told to dial is not knowable from here.
|
|
50
|
+
* - **Whatever host services the orchestrator itself directed the agent at**,
|
|
51
|
+
* which today means its object storage.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveAgentHostAccess(input: {
|
|
54
|
+
policy: NetworkPolicy | undefined;
|
|
55
|
+
orchestratorUrl: string;
|
|
56
|
+
gateway: string;
|
|
57
|
+
hostServices?: string[];
|
|
58
|
+
}): string[];
|
|
59
|
+
//# sourceMappingURL=host-access.d.ts.map
|
package/dist/scaler/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { FirecrackerScalerBackend } from './firecracker-backend.js';
|
|
|
16
16
|
export type { FirecrackerScalerBackendOptions, FirecrackerManagedAgent, } from './firecracker-backend.js';
|
|
17
17
|
export { createScalerBackend, requiredToolsFor } from './backend-factory.js';
|
|
18
18
|
export type { BackendFactoryContext, IpAllocatorParams } from './backend-factory.js';
|
|
19
|
+
export { resolveAgentHostAccess, storageHostAccessEntries } from './host-access.js';
|
|
19
20
|
export { EventScalerBackend } from './event-backend.js';
|
|
20
21
|
export type { EventScalerBackendOptions, ScalerEventEmitterLike } from './event-backend.js';
|
|
21
22
|
export { ClaimStore, DEFAULT_CLAIM_TTL_SECONDS } from './claim-store.js';
|
package/dist/scaler/manager.d.ts
CHANGED
|
@@ -198,6 +198,12 @@ export interface ScalerManagerDeps {
|
|
|
198
198
|
activeJobs: number;
|
|
199
199
|
registeredAt: number;
|
|
200
200
|
}>;
|
|
201
|
+
/** Whether one agent id is currently registered on this host. */
|
|
202
|
+
isRegistered(agentId: string): boolean;
|
|
203
|
+
/** Condemn an agent before tearing it down; returns its active-job count. */
|
|
204
|
+
markDraining(agentId: string): number | undefined;
|
|
205
|
+
/** Undo `markDraining` when the teardown did not happen. */
|
|
206
|
+
clearDraining(agentId: string): void;
|
|
201
207
|
};
|
|
202
208
|
/**
|
|
203
209
|
* Id of the orchestrator instance this manager runs on. Rows this
|
|
@@ -778,6 +784,33 @@ export declare class ScalerManager {
|
|
|
778
784
|
* which is always `releaseAll(agentId)`.
|
|
779
785
|
*/
|
|
780
786
|
private reserveForSpawn;
|
|
787
|
+
/**
|
|
788
|
+
* How many reservations one scaler currently holds.
|
|
789
|
+
*
|
|
790
|
+
* Derived from `this.reservations` rather than kept as a counter beside it:
|
|
791
|
+
* the map IS the admitted set, so a derived count cannot drift out of step
|
|
792
|
+
* with it — including on the boot/leader-switch rehydrate path, which
|
|
793
|
+
* repopulates the map directly.
|
|
794
|
+
*/
|
|
795
|
+
/**
|
|
796
|
+
* Record a refusal taken on one of the two agent-count caps.
|
|
797
|
+
*
|
|
798
|
+
* The refusal itself is returned to the dispatcher as `at-capacity`, which
|
|
799
|
+
* leaves the job queued and logs nothing — correct while agents are genuinely
|
|
800
|
+
* busy, and indistinguishable from a scaler that has stopped spawning
|
|
801
|
+
* altogether. So the counter the metric's own help text promises
|
|
802
|
+
* (`maxAgents`, `globalMaxAgents`) is incremented here, beside the resource
|
|
803
|
+
* caps that already increment it.
|
|
804
|
+
*
|
|
805
|
+
* The warning is narrower than the counter on purpose. Ordinary saturation
|
|
806
|
+
* refuses with live agents holding the cap, and warning on that would emit a
|
|
807
|
+
* line per queued job for as long as the cluster is busy. A refusal where the
|
|
808
|
+
* reservation ledger outruns every backend's active count is the other shape:
|
|
809
|
+
* capacity billed to agents that are not running, which does not resolve on
|
|
810
|
+
* its own.
|
|
811
|
+
*/
|
|
812
|
+
private reportCountCapRefusal;
|
|
813
|
+
private reservedCountForScaler;
|
|
781
814
|
/**
|
|
782
815
|
* Run a critical section under the reservation lock. Serializes concurrent
|
|
783
816
|
* `requestScale()` calls so the check+reserve sequence is atomic.
|
|
@@ -1248,10 +1281,25 @@ export declare class ScalerManager {
|
|
|
1248
1281
|
*/
|
|
1249
1282
|
getScalerContextForAgent(agentId: string): Record<string, unknown> | undefined;
|
|
1250
1283
|
/**
|
|
1251
|
-
* Start the
|
|
1252
|
-
*
|
|
1284
|
+
* Start the machine-pool ledger reaper and the retirement sweep that tears
|
|
1285
|
+
* down drained retiring backends, and publish the warm-pool gauges so each
|
|
1286
|
+
* pool's target is visible before its first fill.
|
|
1287
|
+
*
|
|
1288
|
+
* The warm pools themselves are NOT started here — see
|
|
1289
|
+
* {@link startWarmPools}, which the host calls once every dependency a fill
|
|
1290
|
+
* can reach is wired.
|
|
1253
1291
|
*/
|
|
1254
1292
|
start(): void;
|
|
1293
|
+
/**
|
|
1294
|
+
* Run the warm pools' first deficit pass and arm their periodic tick.
|
|
1295
|
+
*
|
|
1296
|
+
* Separate from {@link start} because the first pass spawns immediately,
|
|
1297
|
+
* and on an event backend a spawn emits through the event emitter — which
|
|
1298
|
+
* the coordinator assigns after the scaler is built. The coordinator calls
|
|
1299
|
+
* this once the emitter exists; the worker, which hosts no event backend,
|
|
1300
|
+
* calls it right after `start()`.
|
|
1301
|
+
*/
|
|
1302
|
+
startWarmPools(): void;
|
|
1255
1303
|
/**
|
|
1256
1304
|
* Provision/heal every backend's host prerequisites before spawning starts.
|
|
1257
1305
|
* Awaits each backend's optional ensureHostReady, catching per-backend so one
|
|
@@ -1352,6 +1400,7 @@ export declare class ScalerManager {
|
|
|
1352
1400
|
* coord boot or Raft leader switch. Reconstructs:
|
|
1353
1401
|
*
|
|
1354
1402
|
* - `spawningAgents` (with `boundJobId` preserved for eager-dispatch on register)
|
|
1403
|
+
* — event-backend rows only; a local-backend row is dropped, see below
|
|
1355
1404
|
* - `agentJobCorrelation` (so scaler-lifecycle events route correctly)
|
|
1356
1405
|
* - `reservations` + `perScalerUsage` (so the cap-check critical
|
|
1357
1406
|
* section reflects the cluster-wide truth, not the local empty
|
|
@@ -374,8 +374,21 @@ export declare class ScalerStateStore {
|
|
|
374
374
|
*/
|
|
375
375
|
export interface ScalerStateRecovery {
|
|
376
376
|
spawningAgentsRehydrated: number;
|
|
377
|
+
/**
|
|
378
|
+
* Spawn rows released during recovery because their backend is local
|
|
379
|
+
* (container / firecracker / bare-metal): such a spawn cannot outlive the
|
|
380
|
+
* process that started it, so nothing could ever register against it, and
|
|
381
|
+
* rehydrating it would hold its reservation until the stale prune.
|
|
382
|
+
*/
|
|
383
|
+
spawningDropped: number;
|
|
377
384
|
agentJobsRehydrated: number;
|
|
378
385
|
reservationsRehydrated: number;
|
|
386
|
+
/**
|
|
387
|
+
* Reservation rows discarded during recovery because no rehydrated spawn
|
|
388
|
+
* backs them. Counted separately from `reservationsRehydrated` so a
|
|
389
|
+
* deployment carrying orphaned rows says so on every boot until it is clean.
|
|
390
|
+
*/
|
|
391
|
+
reservationsDropped: number;
|
|
379
392
|
bufferedEventsLost: number;
|
|
380
393
|
}
|
|
381
394
|
/**
|