@kici-dev/orchestrator 0.13.0 → 0.14.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 +17 -0
- package/dist/cancel/peer-job-cancel.d.ts +12 -0
- package/dist/cli/api-client.d.ts +73 -0
- package/dist/cli/commands/cluster-settings.d.ts +31 -1
- package/dist/cli/commands/peer.d.ts +27 -1
- package/dist/cli/commands/scaler.d.ts +40 -4
- package/dist/cli.js +2210 -933
- package/dist/cluster/cluster-settings-reader.d.ts +8 -2
- package/dist/cluster/coordinator.d.ts +34 -8
- package/dist/cluster/peer-client.d.ts +37 -4
- package/dist/cluster/peer-forget.d.ts +138 -0
- package/dist/cluster/peer-handler.d.ts +18 -1
- package/dist/cluster/scaler-orphans-peer.d.ts +40 -0
- package/dist/cluster/settings-propagation.d.ts +85 -0
- package/dist/cluster/worker-cluster-settings.d.ts +29 -0
- package/dist/config.d.ts +9 -0
- package/dist/db/migrations/154_cluster_settings_firecracker_api_socket_wait.d.ts +4 -0
- package/dist/db/migrations/155_org_settings_reroute_spawn_retry.d.ts +17 -0
- package/dist/db/migrations/156_kici_events_match_outcome.d.ts +17 -0
- package/dist/db/types.d.ts +16 -0
- package/dist/events/event-router.d.ts +7 -1
- package/dist/events/event-store.d.ts +20 -3
- package/dist/events/types.d.ts +45 -0
- package/dist/helpers/windows-batch.d.ts +59 -0
- package/dist/index.js +60 -0
- package/dist/metrics/prometheus.d.ts +2 -0
- package/dist/orchestrator-core.d.ts +7 -0
- package/dist/registration/registration-store.d.ts +10 -0
- package/dist/routes/admin-cluster-settings.d.ts +13 -1
- package/dist/routes/admin-event-inspect.d.ts +36 -0
- package/dist/routes/admin-peer-forget.d.ts +43 -0
- package/dist/routes/admin-scaler-orphans.d.ts +64 -0
- package/dist/routes/admin.d.ts +23 -0
- package/dist/scaler/backend-factory.d.ts +6 -1
- package/dist/scaler/bare-metal-backend.d.ts +17 -15
- package/dist/scaler/bare-metal-launch.d.ts +90 -0
- package/dist/scaler/firecracker-backend.d.ts +181 -7
- package/dist/scaler/live-vms.d.ts +57 -0
- package/dist/scaler/manager.d.ts +112 -3
- package/dist/scaler/orphan-requests.d.ts +23 -0
- package/dist/scaler/overlay-template.d.ts +38 -0
- package/dist/scaler/scaler-state-store.d.ts +7 -0
- package/dist/scaler/spawn-errors.d.ts +17 -0
- package/dist/scaler/startup-orphan-sweep.d.ts +15 -0
- package/dist/scaler/types.d.ts +21 -2
- package/dist/scaler/vm-job-bindings.d.ts +17 -0
- package/dist/secrets/rbac.d.ts +7 -1
- package/dist/server.js +5498 -2193
- package/dist/standalone.js +10754 -7451
- package/dist/worker/in-memory-execution-tracker.d.ts +5 -0
- package/dist/worker/in-memory-job-queue.d.ts +21 -1
- package/dist/worker/queued-reroute-release.d.ts +39 -0
- package/dist/worker/reroute-spawn-control.d.ts +58 -0
- package/dist/worker/spawn-retry-budget.d.ts +87 -0
- package/dist/worker-core.d.ts +11 -2
- package/installer-image-digests.json +3 -3
- package/package.json +4 -4
- package/sbom.spdx.json +53 -53
package/dist/db/types.d.ts
CHANGED
|
@@ -1506,6 +1506,10 @@ export interface KiciEventTable {
|
|
|
1506
1506
|
dlq_at: Date | null;
|
|
1507
1507
|
/** Short DLQ reason: 'exhausted_retries' | 'non_retryable' */
|
|
1508
1508
|
dlq_reason: string | null;
|
|
1509
|
+
/** How the router resolved the event (an `EventMatchOutcome`); NULL until processed */
|
|
1510
|
+
match_outcome: string | null;
|
|
1511
|
+
/** Registrations the event was dispatched to; NULL until processed */
|
|
1512
|
+
matched_count: number | null;
|
|
1509
1513
|
}
|
|
1510
1514
|
export type KiciEvent = Selectable<KiciEventTable>;
|
|
1511
1515
|
export type NewKiciEvent = Insertable<KiciEventTable>;
|
|
@@ -1989,6 +1993,16 @@ export interface OrgSettingsTable {
|
|
|
1989
1993
|
* (config.rerouteMaxHops). Plain INTEGER — pg returns a number on select.
|
|
1990
1994
|
*/
|
|
1991
1995
|
reroute_max_hops: ColumnType<number | null, number | null | undefined, number | null>;
|
|
1996
|
+
/**
|
|
1997
|
+
* Per-org agent spawns a worker attempts for one rerouted job. Null = cluster
|
|
1998
|
+
* default (config.rerouteSpawnMaxAttempts). Plain INTEGER.
|
|
1999
|
+
*/
|
|
2000
|
+
reroute_spawn_max_attempts: ColumnType<number | null, number | null | undefined, number | null>;
|
|
2001
|
+
/**
|
|
2002
|
+
* Per-org wait (ms) after a failed spawn of a rerouted job. Null = cluster
|
|
2003
|
+
* default (config.rerouteSpawnRetryBackoffMs). BIGINT.
|
|
2004
|
+
*/
|
|
2005
|
+
reroute_spawn_retry_backoff_ms: ColumnType<string | null, number | null | undefined, number | null>;
|
|
1992
2006
|
/**
|
|
1993
2007
|
* Per-org staleness threshold (hours) for the DB-backup freshness diagnostic.
|
|
1994
2008
|
* Null = cluster default (config.backupStalenessWarnHours). Plain INTEGER.
|
|
@@ -2197,6 +2211,8 @@ export interface ClusterSettingsTable {
|
|
|
2197
2211
|
* per round.
|
|
2198
2212
|
*/
|
|
2199
2213
|
global_eval_wait_timeout_ms: ColumnType<string | null, number | null | undefined, number | null>;
|
|
2214
|
+
/** How long a Firecracker spawn waits for the VM's API socket. NULL: config default. */
|
|
2215
|
+
firecracker_api_socket_wait_ms: ColumnType<string | null, number | null | undefined, number | null>;
|
|
2200
2216
|
/**
|
|
2201
2217
|
* Retention window in days for `check_run_tracking` rows. The hourly cleanup
|
|
2202
2218
|
* sweep deletes rows untouched for longer than this; 0 disables the sweep.
|
|
@@ -6,7 +6,7 @@ import type { EventStore } from './event-store.js';
|
|
|
6
6
|
import type { EventCircuitBreaker } from './circuit-breaker.js';
|
|
7
7
|
import type { TrustStore } from './trust-store.js';
|
|
8
8
|
import type { RegisteredWorkflow, RegistrationIndex } from '../registration/registration-index.js';
|
|
9
|
-
import type
|
|
9
|
+
import { type EventRouterConfig, type StoredEvent } from './types.js';
|
|
10
10
|
import type { ClusterSettingsReader } from '../cluster/cluster-settings-reader.js';
|
|
11
11
|
/**
|
|
12
12
|
* System events are exempt from the per-source event-storm rate limiter: they
|
|
@@ -213,6 +213,10 @@ export declare class EventRouter {
|
|
|
213
213
|
* schedule a retry or move the event to the DLQ. Swallowing the error
|
|
214
214
|
* here would silently lose the event — exactly the failure mode the
|
|
215
215
|
* lease pattern fixes.
|
|
216
|
+
*
|
|
217
|
+
* Returns how the event resolved, which `dispatchAndRecord` persists on the
|
|
218
|
+
* processed row: `matched` when it dispatched at least one workflow, else
|
|
219
|
+
* the reason nothing ran.
|
|
216
220
|
*/
|
|
217
221
|
private processSubscriptions;
|
|
218
222
|
/**
|
|
@@ -225,6 +229,8 @@ export declare class EventRouter {
|
|
|
225
229
|
* itself dispatched by a failure-lifecycle trigger is neither buffered nor
|
|
226
230
|
* dispatched — a broken notifier must not re-trigger the batch on its own
|
|
227
231
|
* failure. The chain-depth breaker remains the backstop for anything missed.
|
|
232
|
+
*
|
|
233
|
+
* `bufferedCount` counts the decisions appended to a batch window.
|
|
228
234
|
*/
|
|
229
235
|
private bufferBatchDecisions;
|
|
230
236
|
/** Open (or reuse) the registration's batch window and append the failed run. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Kysely, type Transaction } from 'kysely';
|
|
2
2
|
import type { Database } from '../db/types.js';
|
|
3
|
-
import type
|
|
3
|
+
import { EventMatchOutcome, type DlqReason, type EventMatchResult, type EventRouterConfig, type StoredEvent } from './types.js';
|
|
4
4
|
/** Either a Kysely DB handle or an active transaction. */
|
|
5
5
|
export type DbExecutor = Kysely<Database> | Transaction<Database>;
|
|
6
6
|
/**
|
|
@@ -81,11 +81,28 @@ export declare class EventStore {
|
|
|
81
81
|
* re-queries until a page returns fewer than `limit` rows.
|
|
82
82
|
*/
|
|
83
83
|
getUnprocessedSince(sinceId: string | null, limit?: number): Promise<StoredEvent[]>;
|
|
84
|
+
/**
|
|
85
|
+
* List events newest first, for `kici-admin event list`. Every filter is
|
|
86
|
+
* optional except the limit. `before` keeps events created before a
|
|
87
|
+
* timestamp. `afterEventId` is the next-page cursor: the events that sort
|
|
88
|
+
* after that event in this order, compared on the stored `(created_at, id)`
|
|
89
|
+
* so events created in the same millisecond are neither skipped nor repeated.
|
|
90
|
+
*/
|
|
91
|
+
list(opts: {
|
|
92
|
+
name?: string;
|
|
93
|
+
outcome?: EventMatchOutcome;
|
|
94
|
+
since?: Date;
|
|
95
|
+
before?: Date;
|
|
96
|
+
afterEventId?: string;
|
|
97
|
+
limit: number;
|
|
98
|
+
sourceRoutingKey?: string;
|
|
99
|
+
}): Promise<StoredEvent[]>;
|
|
84
100
|
/**
|
|
85
101
|
* Mark an event as fully processed. Called after a successful dispatch.
|
|
86
|
-
* Clears any prior lease so the row is unambiguously terminal
|
|
102
|
+
* Clears any prior lease so the row is unambiguously terminal, and records
|
|
103
|
+
* how the router resolved it when `result` is given.
|
|
87
104
|
*/
|
|
88
|
-
markProcessed(id: string): Promise<void>;
|
|
105
|
+
markProcessed(id: string, result?: EventMatchResult): Promise<void>;
|
|
89
106
|
/**
|
|
90
107
|
* Atomically take a dispatch lease on an event. Replaces the previous
|
|
91
108
|
* "flip processed=true upfront" pattern that silently lost events when
|
package/dist/events/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Event routing configuration and types.
|
|
3
3
|
*/
|
|
4
|
+
import { z } from 'zod';
|
|
4
5
|
/**
|
|
5
6
|
* Configuration for the event routing system.
|
|
6
7
|
*/
|
|
@@ -45,6 +46,22 @@ export interface EventRouterConfig {
|
|
|
45
46
|
* Reason an event landed in the DLQ.
|
|
46
47
|
*/
|
|
47
48
|
export type DlqReason = 'exhausted_retries' | 'non_retryable';
|
|
49
|
+
/** How the router resolved one event against the registered workflows. */
|
|
50
|
+
export declare const EventMatchOutcome: z.ZodEnum<{
|
|
51
|
+
buffered: "buffered";
|
|
52
|
+
matched: "matched";
|
|
53
|
+
"no-registration": "no-registration";
|
|
54
|
+
"no-target-repo": "no-target-repo";
|
|
55
|
+
"no-trigger-match": "no-trigger-match";
|
|
56
|
+
"trust-blocked": "trust-blocked";
|
|
57
|
+
}>;
|
|
58
|
+
export type EventMatchOutcome = z.infer<typeof EventMatchOutcome>;
|
|
59
|
+
/** What the router resolved for one event, persisted on its processed row. */
|
|
60
|
+
export interface EventMatchResult {
|
|
61
|
+
outcome: EventMatchOutcome;
|
|
62
|
+
/** Registrations the event was dispatched to. */
|
|
63
|
+
matchedCount: number;
|
|
64
|
+
}
|
|
48
65
|
/**
|
|
49
66
|
* A stored internal event (matches DB row shape but with JS types).
|
|
50
67
|
*/
|
|
@@ -70,7 +87,35 @@ export interface StoredEvent {
|
|
|
70
87
|
nextRetryAt: Date | null;
|
|
71
88
|
dlqAt: Date | null;
|
|
72
89
|
dlqReason: DlqReason | null;
|
|
90
|
+
/**
|
|
91
|
+
* How the router resolved the event. Set when the event is processed; null
|
|
92
|
+
* before, and for rows processed before the column existed.
|
|
93
|
+
*/
|
|
94
|
+
matchOutcome: EventMatchOutcome | null;
|
|
95
|
+
/** Registrations the event was dispatched to; null when `matchOutcome` is. */
|
|
96
|
+
matchedCount: number | null;
|
|
73
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* An event payload safe to show an operator. An event-scaler scale-up carries
|
|
100
|
+
* a single-use claim code an agent redeems for credentials, so every read
|
|
101
|
+
* surface (event show, the dead-letter queue) replaces it with `[redacted]`.
|
|
102
|
+
*/
|
|
103
|
+
export declare function redactEventPayload(payload: Record<string, unknown>): Record<string, unknown>;
|
|
104
|
+
/** Where an internal event is in its dispatch lifecycle. */
|
|
105
|
+
export declare const EventProcessingState: z.ZodEnum<{
|
|
106
|
+
dlq: "dlq";
|
|
107
|
+
leased: "leased";
|
|
108
|
+
pending: "pending";
|
|
109
|
+
processed: "processed";
|
|
110
|
+
retrying: "retrying";
|
|
111
|
+
}>;
|
|
112
|
+
export type EventProcessingState = z.infer<typeof EventProcessingState>;
|
|
113
|
+
/**
|
|
114
|
+
* Derive an event's dispatch state from its row: DLQ wins, then processed;
|
|
115
|
+
* an unprocessed row is leased while a node holds it, retrying once a failed
|
|
116
|
+
* dispatch scheduled another attempt, and pending before its first lease.
|
|
117
|
+
*/
|
|
118
|
+
export declare function eventProcessingState(e: StoredEvent): EventProcessingState;
|
|
74
119
|
/**
|
|
75
120
|
* Default event router configuration.
|
|
76
121
|
*/
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running a Windows batch file (`.cmd` / `.bat`) through cmd.exe.
|
|
3
|
+
*
|
|
4
|
+
* Node.js refuses to start a batch file without a shell (it throws `EINVAL`),
|
|
5
|
+
* and every launcher a KiCI package ships on Windows is a batch file. Two
|
|
6
|
+
* callers run one: the service installer, which registers the command with
|
|
7
|
+
* shawl, and the bare-metal scaler, which spawns it.
|
|
8
|
+
*
|
|
9
|
+
* Both quote a token on cmd.exe's command line only when it contains a space
|
|
10
|
+
* or a tab — shawl does, and so does libuv when Node.js builds a Windows
|
|
11
|
+
* command line — so one safety rule fits both.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* The cmd.exe arguments that run a batch-file launcher, ahead of its path.
|
|
15
|
+
*
|
|
16
|
+
* shawl stops the service with a ctrl-C to its console, and cmd.exe gets it
|
|
17
|
+
* too: when the batch file's node process exits, cmd.exe asks "Terminate batch
|
|
18
|
+
* job (Y/N)?" and waits for an answer. shawl sees the service exit only when its
|
|
19
|
+
* stop timeout kills cmd.exe. With stdin from NUL ({@link BATCH_STDIN}), cmd.exe
|
|
20
|
+
* reads end-of-input at that prompt and exits with node.
|
|
21
|
+
*
|
|
22
|
+
* `call` keeps the launcher path out of the first position after `/c`, where
|
|
23
|
+
* cmd.exe strips the quotes from a path that contains `(`, `)` or `&`. `/e:on`
|
|
24
|
+
* and `/v:off` pin command extensions (the launchers use `%~dp0`) and delayed
|
|
25
|
+
* expansion whatever the host's registry defaults are.
|
|
26
|
+
*/
|
|
27
|
+
export declare const BATCH_WRAPPER_ARGS: readonly ['/d', '/e:on', '/v:off', '/c', 'call'];
|
|
28
|
+
/** The redirect that closes {@link BATCH_WRAPPER_ARGS}: the launcher's stdin is NUL. */
|
|
29
|
+
export declare const BATCH_STDIN = "<NUL";
|
|
30
|
+
/** A `.cmd` or `.bat` file, which Windows runs through cmd.exe. */
|
|
31
|
+
export declare function isBatchFile(executablePath: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The cmd.exe that COMSPEC names in `env`, as Node itself uses to run a shell on
|
|
34
|
+
* Windows. A COMSPEC that names another shell is not used: the wrapper
|
|
35
|
+
* arguments are cmd.exe's.
|
|
36
|
+
*/
|
|
37
|
+
export declare function cmdExePath(env?: NodeJS.ProcessEnv): string;
|
|
38
|
+
/**
|
|
39
|
+
* Why cmd.exe would read part of `token` as syntax, or null when it would not.
|
|
40
|
+
*
|
|
41
|
+
* A token is quoted on cmd.exe's command line only when it contains a space or
|
|
42
|
+
* a tab. Inside quotes cmd.exe reads `&`, `|`, `<`, `>`, `(`, `)` and the `,`
|
|
43
|
+
* `;` `=` delimiters as text; outside quotes it reads them as syntax. `%` and
|
|
44
|
+
* `^` change even inside quotes (`call` expands `%` again and doubles `^`), and
|
|
45
|
+
* a `"` ends the quoting.
|
|
46
|
+
*
|
|
47
|
+
* @param use - what the token runs as, completing "cannot run <token> …".
|
|
48
|
+
* @param remedy - the sentence that tells the operator what to change.
|
|
49
|
+
*/
|
|
50
|
+
export declare function cmdSafetyRefusal(token: string, use: string, remedy: string): string | null;
|
|
51
|
+
/** Throw the {@link cmdSafetyRefusal} message for `token`, if it has one. */
|
|
52
|
+
export declare function assertCmdSafe(token: string, use: string, remedy: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* The cmd.exe command that runs `command` — a batch file and its arguments —
|
|
55
|
+
* with stdin from NUL. The caller checks each token with
|
|
56
|
+
* {@link assertCmdSafe} first.
|
|
57
|
+
*/
|
|
58
|
+
export declare function batchFileCommand(command: readonly string[], env?: NodeJS.ProcessEnv): string[];
|
|
59
|
+
//# sourceMappingURL=windows-batch.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -2860,6 +2860,51 @@ var AdminApiClient = class {
|
|
|
2860
2860
|
async drainStatus() {
|
|
2861
2861
|
return this.request("GET", "/api/v1/admin/orchestrator/drain");
|
|
2862
2862
|
}
|
|
2863
|
+
/** Forget a departed peer on this coordinator and every connected sibling. */
|
|
2864
|
+
/**
|
|
2865
|
+
* Forget a departed peer on this coordinator and every connected sibling. A
|
|
2866
|
+
* refusal that only needs the backstop acknowledgement comes back as a body
|
|
2867
|
+
* with `acknowledgementRequired`, so the caller can ask and retry; every
|
|
2868
|
+
* other failure throws.
|
|
2869
|
+
*/
|
|
2870
|
+
async forgetPeer(body) {
|
|
2871
|
+
const res = await fetchAdminApi(`${this.baseUrl}/api/v1/admin/peers/forget`, {
|
|
2872
|
+
method: "POST",
|
|
2873
|
+
headers: {
|
|
2874
|
+
Authorization: `Bearer ${this.token}`,
|
|
2875
|
+
"Content-Type": "application/json"
|
|
2876
|
+
},
|
|
2877
|
+
body: JSON.stringify(body)
|
|
2878
|
+
}, this.baseUrl);
|
|
2879
|
+
const text = await res.text();
|
|
2880
|
+
let parsed;
|
|
2881
|
+
try {
|
|
2882
|
+
parsed = JSON.parse(text);
|
|
2883
|
+
} catch {
|
|
2884
|
+
parsed = void 0;
|
|
2885
|
+
}
|
|
2886
|
+
const json = parsed ?? {};
|
|
2887
|
+
if (res.ok) return json;
|
|
2888
|
+
if (res.status === 409 && json.acknowledgementRequired === true) return {
|
|
2889
|
+
instanceId: body.instanceId,
|
|
2890
|
+
results: json.results ?? [],
|
|
2891
|
+
acknowledgementRequired: true,
|
|
2892
|
+
...json.error ? { error: json.error } : {}
|
|
2893
|
+
};
|
|
2894
|
+
throw new Error(`HTTP ${res.status}: ${json.error ?? text}`);
|
|
2895
|
+
}
|
|
2896
|
+
/** List the live Firecracker VMs on this coordinator, or on the `target` peer. */
|
|
2897
|
+
async listScalerOrphans(opts = {}) {
|
|
2898
|
+
const params = new URLSearchParams();
|
|
2899
|
+
if (opts.target !== void 0) params.set("target", opts.target);
|
|
2900
|
+
if (opts.timeoutMs !== void 0) params.set("timeoutMs", String(opts.timeoutMs));
|
|
2901
|
+
const query = params.toString();
|
|
2902
|
+
return this.request("GET", `/api/v1/admin/scaler/orphans${query ? `?${query}` : ""}`);
|
|
2903
|
+
}
|
|
2904
|
+
/** Stop the named VMs on this coordinator, or on the `target` peer. */
|
|
2905
|
+
async stopScalerOrphans(body) {
|
|
2906
|
+
return this.request("POST", "/api/v1/admin/scaler/orphans/stop", body);
|
|
2907
|
+
}
|
|
2863
2908
|
async createApiKey(opts) {
|
|
2864
2909
|
return this.request("POST", "/api/v1/api-keys", opts);
|
|
2865
2910
|
}
|
|
@@ -2986,6 +3031,21 @@ var AdminApiClient = class {
|
|
|
2986
3031
|
if (opts.routingKey) params.set("routingKey", opts.routingKey);
|
|
2987
3032
|
return this.request("GET", `/api/v1/admin/event-log/${encodeURIComponent(deliveryId)}?${params}`);
|
|
2988
3033
|
}
|
|
3034
|
+
/** List internal events, newest first (`GET /api/v1/admin/events`). */
|
|
3035
|
+
async listEvents(opts) {
|
|
3036
|
+
const params = new URLSearchParams();
|
|
3037
|
+
if (opts?.name) params.set("name", opts.name);
|
|
3038
|
+
if (opts?.outcome) params.set("outcome", opts.outcome);
|
|
3039
|
+
if (opts?.since) params.set("since", opts.since);
|
|
3040
|
+
if (opts?.before) params.set("before", opts.before);
|
|
3041
|
+
if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
|
|
3042
|
+
const qs = params.toString();
|
|
3043
|
+
return this.request("GET", `/api/v1/admin/events${qs ? `?${qs}` : ""}`);
|
|
3044
|
+
}
|
|
3045
|
+
/** One internal event with its redacted payload and dispatched runs. */
|
|
3046
|
+
async getEvent(id) {
|
|
3047
|
+
return this.request("GET", `/api/v1/admin/events/${encodeURIComponent(id)}`);
|
|
3048
|
+
}
|
|
2989
3049
|
async listAccessLog(opts) {
|
|
2990
3050
|
const params = new URLSearchParams();
|
|
2991
3051
|
if (opts?.orgId) params.set("orgId", opts.orgId);
|
|
@@ -539,6 +539,8 @@ export declare const eventRetryTotal: import("@opentelemetry/api").Counter<impor
|
|
|
539
539
|
* - reason: why the event was DLQ'd (exhausted_retries)
|
|
540
540
|
*/
|
|
541
541
|
export declare const eventDlqTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
|
|
542
|
+
/** Record a reserved internal event that matched no subscriber. */
|
|
543
|
+
export declare function incEventUnmatched(eventName: string, reason: string): void;
|
|
542
544
|
/**
|
|
543
545
|
* Startup catch-up scans that threw. Emits no labels.
|
|
544
546
|
*
|
|
@@ -41,6 +41,7 @@ import { ArtifactStore } from './artifacts/artifact-store.js';
|
|
|
41
41
|
import { CheckRunReporter } from './reporting/check-run-reporter.js';
|
|
42
42
|
import type { ProcessingDeps } from './pipeline/processor.js';
|
|
43
43
|
import type { InvokeGateDeps, SummonArgs, SummonedRun } from './pipeline/invoke-gate.js';
|
|
44
|
+
import { type PeerForgetRequestHandler } from './cluster/peer-forget.js';
|
|
44
45
|
import { StepLogBuffer } from './reporting/step-log-buffer.js';
|
|
45
46
|
import { type LogStorage } from './reporting/log-storage.js';
|
|
46
47
|
import { ExecutionTracker, type ExecutionTrackerDeps } from './reporting/execution-tracker.js';
|
|
@@ -167,6 +168,12 @@ export interface OrchestratorSubsystems {
|
|
|
167
168
|
orphanRecovery: OrphanRecovery;
|
|
168
169
|
peerHandler: ReturnType<typeof createPeerHandler>;
|
|
169
170
|
peerClients: Map<string, PeerClient>;
|
|
171
|
+
/**
|
|
172
|
+
* Answers a `kici-admin peer forget` a sibling coordinator fanned out. Wired
|
|
173
|
+
* into every outgoing PeerClient (the incoming peer-handler is wired directly
|
|
174
|
+
* in setupCluster).
|
|
175
|
+
*/
|
|
176
|
+
answerPeerForgetRequest: PeerForgetRequestHandler;
|
|
170
177
|
getLocalInventory: () => Omit<PeerHeartbeat, 'type'>;
|
|
171
178
|
broadcastHeartbeatToAllPeers: () => void;
|
|
172
179
|
broadcastAgentTokenRevoke: (tokenId: string) => void;
|
|
@@ -74,6 +74,16 @@ interface ReplaceAllOptions {
|
|
|
74
74
|
depCacheKey?: DepCacheKey;
|
|
75
75
|
/** Set of workflow names that should be marked as global */
|
|
76
76
|
globalWorkflowNames?: Set<string>;
|
|
77
|
+
/**
|
|
78
|
+
* Delete this routing key's rows for the repo whose workflow the incoming set
|
|
79
|
+
* does not name. `true` (the default) is the default-branch push sync: the
|
|
80
|
+
* lock file is the whole truth for the repo. The manual registration route
|
|
81
|
+
* passes `false`: it upserts the workflows it names and leaves every other
|
|
82
|
+
* registration of the repo alone, which is what
|
|
83
|
+
* `kici-admin workflow register-manual` documents and what its direct-DB
|
|
84
|
+
* mode does.
|
|
85
|
+
*/
|
|
86
|
+
prune?: boolean;
|
|
77
87
|
}
|
|
78
88
|
/**
|
|
79
89
|
* DB CRUD for workflow_registrations table with atomic upsert-based replace-all.
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Exposes GET / PATCH `/api/v1/admin/cluster-settings` so `kici-admin
|
|
5
5
|
* cluster-settings` can read and write the single `cluster_settings` row
|
|
6
6
|
* (id='default') without going through the Platform dashboard proxy — the CLI
|
|
7
|
-
* stays operable even when Platform is unavailable.
|
|
7
|
+
* stays operable even when Platform is unavailable. `GET
|
|
8
|
+
* /api/v1/admin/cluster-settings/propagation` reports which settings version
|
|
9
|
+
* each orchestrator this coordinator knows has applied.
|
|
8
10
|
*
|
|
9
11
|
* These knobs are fleet-wide, not per-tenant, so the route is deliberately NOT
|
|
10
12
|
* under `/orgs/:customerId/*`. Mutations require the `secret.write` RBAC
|
|
@@ -15,9 +17,19 @@ import { Hono } from 'hono';
|
|
|
15
17
|
import { type Kysely } from 'kysely';
|
|
16
18
|
import type { Database } from '../db/types.js';
|
|
17
19
|
import type { RbacEnforcer, Role } from '../secrets/rbac.js';
|
|
20
|
+
import type { PeerRegistry } from '../cluster/peer-registry.js';
|
|
21
|
+
/** The live view behind `GET /cluster-settings/propagation`. */
|
|
22
|
+
export interface ClusterSettingsPropagationSource {
|
|
23
|
+
instanceId: string;
|
|
24
|
+
peerRegistry: Pick<PeerRegistry, 'getAllPeers'>;
|
|
25
|
+
/** The cluster-settings version this coordinator's reads currently serve. */
|
|
26
|
+
localVersion: () => number;
|
|
27
|
+
}
|
|
18
28
|
interface ClusterSettingsRouteDeps {
|
|
19
29
|
db: Kysely<Database>;
|
|
20
30
|
rbac: RbacEnforcer;
|
|
31
|
+
/** Mounts `GET /cluster-settings/propagation` when set. */
|
|
32
|
+
propagation?: ClusterSettingsPropagationSource;
|
|
21
33
|
}
|
|
22
34
|
type AdminEnv = {
|
|
23
35
|
Variables: {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin API routes for inspecting internal events (`kici_events`).
|
|
3
|
+
*
|
|
4
|
+
* GET /api/v1/admin/events — list events, newest first (name, outcome, time filters)
|
|
5
|
+
* GET /api/v1/admin/events/:id — one event: its payload and the runs it dispatched
|
|
6
|
+
*
|
|
7
|
+
* Each row carries its dispatch state (`pending`, `leased`, `retrying`,
|
|
8
|
+
* `processed`, `dlq`) and, once processed, how the router resolved it
|
|
9
|
+
* (`matched`, `no-target-repo`, …) with the number of workflows it reached.
|
|
10
|
+
* Both routes need `event_dlq.read`, the permission that already gates reading
|
|
11
|
+
* `kici_events` rows; the payload on `show` also needs
|
|
12
|
+
* `event_log.read_payload`. A routing-key-scoped token sees only events
|
|
13
|
+
* emitted under its own key.
|
|
14
|
+
*/
|
|
15
|
+
import { Hono } from 'hono';
|
|
16
|
+
import type { Kysely } from 'kysely';
|
|
17
|
+
import type { Database } from '../db/types.js';
|
|
18
|
+
import type { EventStore } from '../events/event-store.js';
|
|
19
|
+
import type { TokenManager } from '../secrets/token-manager.js';
|
|
20
|
+
import type { RbacEnforcer, Role } from '../secrets/rbac.js';
|
|
21
|
+
export interface AdminEventInspectRoutesDeps {
|
|
22
|
+
db: Kysely<Database>;
|
|
23
|
+
eventStore: EventStore;
|
|
24
|
+
tokenManager: TokenManager;
|
|
25
|
+
rbac: RbacEnforcer;
|
|
26
|
+
}
|
|
27
|
+
type AdminEnv = {
|
|
28
|
+
Variables: {
|
|
29
|
+
role: Role;
|
|
30
|
+
userId: string;
|
|
31
|
+
routingKey: string | null;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export declare function createAdminEventInspectRoutes(deps: AdminEventInspectRoutesDeps): Hono<AdminEnv>;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=admin-event-inspect.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forget a departed peer, behind `kici-admin peer forget <instance-id>`.
|
|
3
|
+
*
|
|
4
|
+
* Mounted inside `createAdminRoutes` at `/api/v1/admin`, so the Bearer-token
|
|
5
|
+
* auth middleware has already resolved the caller's role. Needs an unscoped
|
|
6
|
+
* token and the `peer.manage` permission (owner, admin).
|
|
7
|
+
*
|
|
8
|
+
* - `POST /api/v1/admin/peers/forget { instanceId, timeoutMs? }` — drop the
|
|
9
|
+
* peer from this coordinator's live peer registry, then from every connected
|
|
10
|
+
* sibling coordinator's, and answer with one result per coordinator.
|
|
11
|
+
*
|
|
12
|
+
* Forgetting never touches the peer's credential (`kici-admin peer revoke`
|
|
13
|
+
* refuses its next connection). A peer that connects again later is
|
|
14
|
+
* registered again as usual.
|
|
15
|
+
*/
|
|
16
|
+
import { Hono } from 'hono';
|
|
17
|
+
import type { AccessLogWriter } from '../audit/access-log.js';
|
|
18
|
+
import { type PeerForgetResult } from '../cluster/peer-forget.js';
|
|
19
|
+
import type { RbacEnforcer, Role } from '../secrets/rbac.js';
|
|
20
|
+
type AdminEnv = {
|
|
21
|
+
Variables: {
|
|
22
|
+
role: Role;
|
|
23
|
+
userId: string;
|
|
24
|
+
routingKey: string | null;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
/** What the route needs from the coordinator it runs on. */
|
|
28
|
+
export interface PeerForgetRouteDeps {
|
|
29
|
+
/**
|
|
30
|
+
* Forget `instanceId` here, then on every connected sibling coordinator
|
|
31
|
+
* (skipped when this coordinator keeps the peer). The first result is this
|
|
32
|
+
* coordinator's. `acknowledgeBackstop` lets a forget that switches an
|
|
33
|
+
* event-provision backstop back on go ahead.
|
|
34
|
+
*/
|
|
35
|
+
forget: (instanceId: string, timeoutMs: number, acknowledgeBackstop: boolean) => Promise<PeerForgetResult[]>;
|
|
36
|
+
}
|
|
37
|
+
export declare function createPeerForgetRoutes(deps: {
|
|
38
|
+
peerForget: PeerForgetRouteDeps;
|
|
39
|
+
rbac: RbacEnforcer;
|
|
40
|
+
accessLog?: AccessLogWriter;
|
|
41
|
+
}): Hono<AdminEnv>;
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=admin-peer-forget.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live orphaned Firecracker VM admin routes, behind `kici-admin scaler orphans`.
|
|
3
|
+
*
|
|
4
|
+
* Mounted inside `createAdminRoutes` at `/api/v1/admin`, so the Bearer-token
|
|
5
|
+
* auth middleware has already resolved the caller's role. Both routes need an
|
|
6
|
+
* unscoped token: a VM belongs to a host, not to one routing key.
|
|
7
|
+
*
|
|
8
|
+
* - `GET /api/v1/admin/scaler/orphans?target=<instance-id>&timeoutMs=<n>` —
|
|
9
|
+
* every live Firecracker VM on the node, with how it is tracked
|
|
10
|
+
* (`scaler.read`).
|
|
11
|
+
* - `POST /api/v1/admin/scaler/orphans/stop { target?, vmIds, timeoutMs? }` —
|
|
12
|
+
* stop the approved VMs nothing tracks (`scaler.manage`).
|
|
13
|
+
*
|
|
14
|
+
* The node is this coordinator, or the peer `target` names, reached over the
|
|
15
|
+
* authenticated peer channel. The coordinator adds the one tracker only it
|
|
16
|
+
* can answer — a non-terminal job in its dispatch queue bound to the VM's
|
|
17
|
+
* agent — and never forwards a bound VM to the node's stop.
|
|
18
|
+
*/
|
|
19
|
+
import { Hono } from 'hono';
|
|
20
|
+
import { type PeerScalerOrphansResponse } from '@kici-dev/engine';
|
|
21
|
+
import type { AccessLogWriter } from '../audit/access-log.js';
|
|
22
|
+
import type { RbacEnforcer, Role } from '../secrets/rbac.js';
|
|
23
|
+
import { type ScalerOrphansAnswer, type ScalerOrphansRequest } from '../scaler/orphan-requests.js';
|
|
24
|
+
import { ScalerOrphansForwardFailure } from '../cluster/scaler-orphans-peer.js';
|
|
25
|
+
type AdminEnv = {
|
|
26
|
+
Variables: {
|
|
27
|
+
role: Role;
|
|
28
|
+
userId: string;
|
|
29
|
+
routingKey: string | null;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/** What the routes need from the coordinator they run on. */
|
|
33
|
+
export interface ScalerOrphansRouteDeps {
|
|
34
|
+
/** This coordinator's instance id. */
|
|
35
|
+
instanceId: string;
|
|
36
|
+
role: 'coordinator' | 'worker';
|
|
37
|
+
/** The cluster role a connected peer registered with, if known. */
|
|
38
|
+
peerRole: (instanceId: string) => 'coordinator' | 'worker' | undefined;
|
|
39
|
+
/** Answer from this coordinator's own host and tracking. */
|
|
40
|
+
answerLocal: (req: ScalerOrphansRequest) => Promise<ScalerOrphansAnswer>;
|
|
41
|
+
/** Forward to a peer over the peer channel. */
|
|
42
|
+
forward: (target: string, req: ScalerOrphansRequest, timeoutMs: number) => Promise<PeerScalerOrphansResponse | ScalerOrphansForwardFailure>;
|
|
43
|
+
/** Non-terminal dispatch-queue jobs bound to these agent ids, keyed by agent id. */
|
|
44
|
+
findBindings: (agentIds: string[]) => Promise<Map<string, string>>;
|
|
45
|
+
/**
|
|
46
|
+
* The cluster instance each of these agent ids is registered with — this
|
|
47
|
+
* coordinator or a peer whose heartbeat lists it — keyed by agent id. An
|
|
48
|
+
* agent can register with an HA sibling of the node that runs its VM.
|
|
49
|
+
*/
|
|
50
|
+
findRegistrations: (agentIds: string[]) => Map<string, string>;
|
|
51
|
+
/**
|
|
52
|
+
* The coordinator peers this cluster expects but has no live link to. Their
|
|
53
|
+
* registered agents are unknown, so while any is missing no VM can be shown
|
|
54
|
+
* to be untracked.
|
|
55
|
+
*/
|
|
56
|
+
disconnectedCoordinators: () => string[];
|
|
57
|
+
}
|
|
58
|
+
export declare function createScalerOrphansRoutes(deps: {
|
|
59
|
+
orphans: ScalerOrphansRouteDeps;
|
|
60
|
+
rbac: RbacEnforcer;
|
|
61
|
+
accessLog?: AccessLogWriter;
|
|
62
|
+
}): Hono<AdminEnv>;
|
|
63
|
+
export {};
|
|
64
|
+
//# sourceMappingURL=admin-scaler-orphans.d.ts.map
|
package/dist/routes/admin.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ import type { AgentRegistry } from '../agent/registry.js';
|
|
|
18
18
|
import type { SharedConfigStore } from '../config/shared-store.js';
|
|
19
19
|
import type { ResolvedMasterKeys } from '../secrets/config.js';
|
|
20
20
|
import { type HeldRunReleaseWiring } from './admin-held-runs.js';
|
|
21
|
+
import { type ClusterSettingsPropagationSource } from './admin-cluster-settings.js';
|
|
22
|
+
import { type ScalerOrphansRouteDeps } from './admin-scaler-orphans.js';
|
|
23
|
+
import { type PeerForgetRouteDeps } from './admin-peer-forget.js';
|
|
21
24
|
import type { DrainController } from '../drain/drain-controller.js';
|
|
22
25
|
import type { SourceStore } from '../sources/source-store.js';
|
|
23
26
|
import type { JoinTokenManager } from '../cluster/join-token.js';
|
|
@@ -159,6 +162,26 @@ export interface AdminRouteDeps {
|
|
|
159
162
|
* write is best-effort, never gating.
|
|
160
163
|
*/
|
|
161
164
|
accessLog?: AccessLogWriter;
|
|
165
|
+
/**
|
|
166
|
+
* Optional -- the coordinator's live-VM listing and stop, for this node or a
|
|
167
|
+
* `--target` peer. Backs `GET /api/v1/admin/scaler/orphans` and
|
|
168
|
+
* `POST /api/v1/admin/scaler/orphans/stop` (`kici-admin scaler orphans`).
|
|
169
|
+
* Bound by orchestrator-core once the cluster is up.
|
|
170
|
+
*/
|
|
171
|
+
scalerOrphans?: ScalerOrphansRouteDeps;
|
|
172
|
+
/**
|
|
173
|
+
* Optional -- forget a departed peer here and on every connected sibling
|
|
174
|
+
* coordinator. Backs `POST /api/v1/admin/peers/forget` (`kici-admin peer
|
|
175
|
+
* forget`). Bound by orchestrator-core once the cluster is up.
|
|
176
|
+
*/
|
|
177
|
+
peerForget?: PeerForgetRouteDeps;
|
|
178
|
+
/**
|
|
179
|
+
* Optional -- the peer view behind
|
|
180
|
+
* `GET /api/v1/admin/cluster-settings/propagation` (the propagation section of
|
|
181
|
+
* `kici-admin cluster-settings show`). Bound by orchestrator-core once the
|
|
182
|
+
* cluster is up; the route is not mounted without it.
|
|
183
|
+
*/
|
|
184
|
+
clusterSettingsPropagation?: ClusterSettingsPropagationSource;
|
|
162
185
|
/**
|
|
163
186
|
* Optional -- fulfil deferred attestations on demand (mints in the running
|
|
164
187
|
* orchestrator process, which owns the Platform WS). Backs
|
|
@@ -38,7 +38,12 @@ export interface BackendFactoryContext {
|
|
|
38
38
|
tokenTtlMs?: number;
|
|
39
39
|
/** Live per-spawn resolver for the fleet-wide agent-token TTL. */
|
|
40
40
|
tokenTtlProvider: () => Promise<number>;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Live per-spawn resolver for the fleet-wide Firecracker API-socket wait.
|
|
43
|
+
* A coordinator reads `cluster_settings`; a worker reads its pulled snapshot.
|
|
44
|
+
*/
|
|
45
|
+
firecrackerApiSocketWaitMsProvider: () => Promise<number>;
|
|
46
|
+
/** DB-backed on a coordinator, in-memory on a worker. */
|
|
42
47
|
ipAllocator: (params: IpAllocatorParams) => IpAllocator;
|
|
43
48
|
/**
|
|
44
49
|
* The already-constructed scaler state store. The event backend's claim
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Bare-metal scaler backend implementation.
|
|
3
3
|
*
|
|
4
4
|
* Manages ephemeral agent processes using child_process.spawn.
|
|
5
|
-
*
|
|
5
|
+
* On Linux and macOS each agent leads its own process group, which teardown
|
|
6
|
+
* signals; on Windows teardown ends the agent's process tree with taskkill.
|
|
6
7
|
* All agents are single-use: destroyed after job completion.
|
|
7
8
|
*/
|
|
8
9
|
import { type ToolRequirement } from '@kici-dev/shared';
|
|
@@ -98,6 +99,11 @@ export declare class BareMetalScalerBackend implements ScalerBackend {
|
|
|
98
99
|
get labelSets(): LabelSetConfig[];
|
|
99
100
|
getActiveCount(): number;
|
|
100
101
|
spawn(labelSet: string[], agentId: string, orchestratorUrl: string, onEvent?: ScalerEventCallback, effectiveLimits?: EffectiveLimits, spawnContext?: SpawnContext, signal?: AbortSignal): Promise<ManagedAgent>;
|
|
102
|
+
/**
|
|
103
|
+
* Record that the agent registered. From then on its exit is a disconnect
|
|
104
|
+
* the manager handles, not a failed spawn.
|
|
105
|
+
*/
|
|
106
|
+
markRegistered(managedId: string): void;
|
|
101
107
|
/**
|
|
102
108
|
* Launch the agent inside the job's own container image.
|
|
103
109
|
*
|
|
@@ -145,23 +151,19 @@ export declare class BareMetalScalerBackend implements ScalerBackend {
|
|
|
145
151
|
entry?: ScalerEntry;
|
|
146
152
|
}): ValidationResult;
|
|
147
153
|
/**
|
|
148
|
-
*
|
|
154
|
+
* End a Windows agent's whole process tree.
|
|
149
155
|
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* - systemd-run scope: returns ('systemd-run', [...]) wrapping the
|
|
155
|
-
* binary in a transient `--user --scope --slice=kici-scaler` cgroup
|
|
156
|
-
* with `CPUQuota=` / `MemoryMax=` properties translated from the
|
|
157
|
-
* effective limits. The scope name embeds `agentId` so each agent is
|
|
158
|
-
* a separately-named transient unit (and `systemctl --user status
|
|
159
|
-
* kici-agent-<agentId>.scope` works).
|
|
156
|
+
* Windows has no process groups to signal and no SIGTERM, and Node.js cannot
|
|
157
|
+
* send a ctrl-C there (`process.kill` terminates the target unconditionally),
|
|
158
|
+
* so the tree is ended at once with `taskkill /T /F`. The pid is cmd.exe's, which runs the agent's `node` and
|
|
159
|
+
* everything it started.
|
|
160
160
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
161
|
+
* Nothing runs once Node.js has seen the child exit: libuv then releases its
|
|
162
|
+
* handle on the process, Windows may give the pid to an unrelated process,
|
|
163
|
+
* and `/T` would end that process's tree. While the child runs, the open
|
|
164
|
+
* handle keeps the pid ours.
|
|
163
165
|
*/
|
|
164
|
-
private
|
|
166
|
+
private endWindowsTree;
|
|
165
167
|
/**
|
|
166
168
|
* Wait for a process to exit within a timeout.
|
|
167
169
|
* Returns true if the process exited, false if timed out.
|