@intx/db 0.3.0 → 0.4.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/README.md +11 -0
- package/dist/approval-store.d.ts +7 -0
- package/dist/approval-store.js +13 -0
- package/dist/backfill-principal-keys.d.ts +9 -0
- package/dist/backfill-principal-keys.js +51 -0
- package/dist/client.d.ts +27 -2
- package/dist/client.js +6 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -0
- package/dist/connection.js +1 -0
- package/dist/credential-resolution.d.ts +54 -5
- package/dist/credential-resolution.js +109 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +10 -4
- package/dist/model-source-resolution.d.ts +6 -5
- package/dist/model-source-resolution.js +42 -13
- package/dist/parse-row.d.ts +39 -8
- package/dist/parse-row.js +8 -0
- package/dist/principal-key-store.d.ts +51 -0
- package/dist/principal-key-store.js +82 -0
- package/dist/principal-store.d.ts +44 -0
- package/dist/principal-store.js +67 -0
- package/dist/schema/approvals.js +5 -0
- package/dist/schema/git-tokens.js +1 -1
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +2 -0
- package/dist/schema/principal-keys.d.ts +126 -0
- package/dist/schema/principal-keys.js +28 -0
- package/dist/schema/sidecar-allocation.d.ts +19 -40
- package/dist/schema/sidecar-allocation.js +4 -8
- package/dist/schema/sidecar.d.ts +0 -17
- package/dist/schema/sidecar.js +4 -12
- package/dist/schema/tenants.js +13 -3
- package/dist/schema/workflow-probe.d.ts +358 -0
- package/dist/schema/workflow-probe.js +51 -0
- package/dist/schema/workflow-run-dispatch.d.ts +17 -0
- package/dist/schema/workflow-run-dispatch.js +7 -0
- package/dist/schema/workflow-run.d.ts +27 -0
- package/dist/schema/workflow-run.js +8 -0
- package/dist/sender-key-resolver.d.ts +91 -0
- package/dist/sender-key-resolver.js +184 -0
- package/dist/sidecar-allocation-store.d.ts +38 -3
- package/dist/sidecar-allocation-store.js +224 -22
- package/dist/signer-identity.d.ts +12 -0
- package/dist/signer-identity.js +15 -0
- package/dist/tenant-hierarchy.d.ts +2 -0
- package/dist/tenant-hierarchy.js +27 -0
- package/dist/workflow-probe-store.d.ts +47 -0
- package/dist/workflow-probe-store.js +124 -0
- package/dist/workflow-run-dispatch-store.d.ts +3 -0
- package/dist/workflow-run-dispatch-store.js +12 -2
- package/migrations/0085_add_approval_run_idx.sql +1 -0
- package/migrations/0086_cool_human_cannonball.sql +1 -0
- package/migrations/0087_drop_sidecar_placement.sql +3 -0
- package/migrations/0088_thick_sprite.sql +32 -0
- package/migrations/0089_tense_selene.sql +13 -0
- package/migrations/0090_tenant_domain_lower_unique.sql +2 -0
- package/migrations/0091_workflow_run_dispatch_sender_address.sql +21 -0
- package/migrations/0092_sidecar_destroy_failed.sql +4 -0
- package/migrations/0093_sidecar_initialization.sql +2 -0
- package/migrations/meta/0085_snapshot.json +4111 -0
- package/migrations/meta/0086_snapshot.json +4117 -0
- package/migrations/meta/0087_snapshot.json +4100 -0
- package/migrations/meta/0088_snapshot.json +4286 -0
- package/migrations/meta/0089_snapshot.json +4376 -0
- package/migrations/meta/0090_snapshot.json +4387 -0
- package/migrations/meta/0091_snapshot.json +4397 -0
- package/migrations/meta/0092_snapshot.json +4397 -0
- package/migrations/meta/0093_snapshot.json +4407 -0
- package/migrations/meta/_journal.json +63 -0
- package/package.json +6 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type } from "arktype";
|
|
2
|
-
import { and, asc, eq, inArray, isNull, lte, or, sql } from "drizzle-orm";
|
|
3
|
-
import {
|
|
2
|
+
import { and, asc, eq, gt, inArray, isNull, lte, notInArray, or, sql, } from "drizzle-orm";
|
|
3
|
+
import { sidecarAllocationStatuses, } from "@intx/types";
|
|
4
4
|
import { createWorkflowRunDispatchStore } from "./workflow-run-dispatch-store.js";
|
|
5
5
|
import { isLiveWorkflowRunStatus, liveWorkflowRunStatuses, principal, sidecar, sidecarAllocation, workflowRun, workflowRunLaunchSpec, } from "./schema/index.js";
|
|
6
6
|
const SidecarAllocationStatusValidator = type.enumerated(...sidecarAllocationStatuses);
|
|
@@ -13,10 +13,6 @@ const activeStatuses = [
|
|
|
13
13
|
"releasing",
|
|
14
14
|
];
|
|
15
15
|
function parseSidecarAllocationRow(row) {
|
|
16
|
-
const placement = SidecarPlacementRequirement.assert({
|
|
17
|
-
sharing: row.placementSharing,
|
|
18
|
-
reuse: row.sidecarReuse,
|
|
19
|
-
});
|
|
20
16
|
return {
|
|
21
17
|
id: row.id,
|
|
22
18
|
anchorRunId: row.anchorRunId,
|
|
@@ -25,7 +21,6 @@ function parseSidecarAllocationRow(row) {
|
|
|
25
21
|
provisionerApiVersion: SidecarProvisionerApiVersion.assert(row.provisionerApiVersion),
|
|
26
22
|
provisionerBindingFingerprint: row.provisionerBindingFingerprint,
|
|
27
23
|
...(row.sidecarId !== null ? { sidecarId: row.sidecarId } : {}),
|
|
28
|
-
placement,
|
|
29
24
|
status: SidecarAllocationStatusValidator.assert(row.status),
|
|
30
25
|
generation: row.generation,
|
|
31
26
|
...(row.ensureAcceptedGeneration !== null
|
|
@@ -39,6 +34,9 @@ function parseSidecarAllocationRow(row) {
|
|
|
39
34
|
...(row.reconciliationLeaseExpiresAt !== null
|
|
40
35
|
? { reconciliationLeaseExpiresAt: row.reconciliationLeaseExpiresAt }
|
|
41
36
|
: {}),
|
|
37
|
+
...(row.initializationLeaseId !== null
|
|
38
|
+
? { initializationLeaseId: row.initializationLeaseId }
|
|
39
|
+
: {}),
|
|
42
40
|
ensureAttempts: row.ensureAttempts,
|
|
43
41
|
destroyAttempts: row.destroyAttempts,
|
|
44
42
|
...(row.connectDeadline !== null
|
|
@@ -58,16 +56,92 @@ function databaseTimestamp(override) {
|
|
|
58
56
|
function leaseCondition(expectedLeaseId) {
|
|
59
57
|
return expectedLeaseId === undefined
|
|
60
58
|
? []
|
|
61
|
-
: [
|
|
59
|
+
: [
|
|
60
|
+
eq(sidecarAllocation.reconciliationLeaseId, expectedLeaseId),
|
|
61
|
+
gt(sidecarAllocation.reconciliationLeaseExpiresAt, sql `clock_timestamp()`),
|
|
62
|
+
];
|
|
62
63
|
}
|
|
63
64
|
export function createSidecarAllocationStore(db) {
|
|
64
65
|
const workflowRunDispatchStore = createWorkflowRunDispatchStore(db);
|
|
66
|
+
function initializationConditions(args, expectedMarker, { requireCurrentLease = true } = {}) {
|
|
67
|
+
return and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.anchorRunId, args.anchorRunId), eq(sidecarAllocation.tenantId, args.tenantId), eq(sidecarAllocation.status, "allocated"), eq(sidecarAllocation.generation, args.generation), eq(sidecarAllocation.ensureAcceptedGeneration, args.generation), ...(requireCurrentLease ? leaseCondition(args.leaseId) : []), expectedMarker === null
|
|
68
|
+
? isNull(sidecarAllocation.initializationLeaseId)
|
|
69
|
+
: eq(sidecarAllocation.initializationLeaseId, expectedMarker));
|
|
70
|
+
}
|
|
71
|
+
async function writeInitialization(args, completion) {
|
|
72
|
+
args.signal.throwIfAborted();
|
|
73
|
+
return db.transaction(async (tx) => {
|
|
74
|
+
const condition = initializationConditions(args, completion === undefined ? null : args.leaseId);
|
|
75
|
+
const [allocation] = await tx
|
|
76
|
+
.select({ id: sidecarAllocation.id })
|
|
77
|
+
.from(sidecarAllocation)
|
|
78
|
+
.where(condition)
|
|
79
|
+
.for("update");
|
|
80
|
+
args.signal.throwIfAborted();
|
|
81
|
+
if (allocation === undefined)
|
|
82
|
+
return null;
|
|
83
|
+
let previousPublicKey = null;
|
|
84
|
+
if (completion === undefined) {
|
|
85
|
+
const [previous] = await tx
|
|
86
|
+
.select({ publicKey: workflowRun.publicKey })
|
|
87
|
+
.from(workflowRun)
|
|
88
|
+
.where(and(eq(workflowRun.id, args.anchorRunId), eq(workflowRun.anchorRunId, args.anchorRunId), eq(workflowRun.tenantId, args.tenantId)))
|
|
89
|
+
.for("update");
|
|
90
|
+
if (previous === undefined)
|
|
91
|
+
throw new Error("Initialization anchor is missing");
|
|
92
|
+
previousPublicKey = previous.publicKey;
|
|
93
|
+
}
|
|
94
|
+
const [anchor] = await tx
|
|
95
|
+
.update(workflowRun)
|
|
96
|
+
.set({
|
|
97
|
+
publicKey: completion?.publicKey ?? null,
|
|
98
|
+
...(completion?.credentialRefs !== undefined
|
|
99
|
+
? { credentialRefs: completion.credentialRefs }
|
|
100
|
+
: {}),
|
|
101
|
+
})
|
|
102
|
+
.where(and(eq(workflowRun.id, args.anchorRunId), eq(workflowRun.anchorRunId, args.anchorRunId), eq(workflowRun.tenantId, args.tenantId)))
|
|
103
|
+
.returning({ id: workflowRun.id });
|
|
104
|
+
if (anchor === undefined)
|
|
105
|
+
throw new Error("Initialization anchor is missing");
|
|
106
|
+
// Check the lease again after waiting on the anchor row. Both writes roll
|
|
107
|
+
// back if ownership expired while the transaction held the allocation lock.
|
|
108
|
+
const [updated] = await tx
|
|
109
|
+
.update(sidecarAllocation)
|
|
110
|
+
.set({
|
|
111
|
+
initializationLeaseId: completion === undefined ? args.leaseId : null,
|
|
112
|
+
})
|
|
113
|
+
.where(condition)
|
|
114
|
+
.returning({ id: sidecarAllocation.id });
|
|
115
|
+
args.signal.throwIfAborted();
|
|
116
|
+
if (updated === undefined)
|
|
117
|
+
throw new Error("Initialization lease expired");
|
|
118
|
+
return { previousPublicKey };
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async function initializationCompleted(tx, args) {
|
|
122
|
+
const [allocation] = await tx
|
|
123
|
+
.select({
|
|
124
|
+
anchorRunId: sidecarAllocation.anchorRunId,
|
|
125
|
+
initializationLeaseId: sidecarAllocation.initializationLeaseId,
|
|
126
|
+
})
|
|
127
|
+
.from(sidecarAllocation)
|
|
128
|
+
.where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.generation, args.expectedGeneration), ...leaseCondition(args.expectedLeaseId)))
|
|
129
|
+
.for("update");
|
|
130
|
+
if (allocation === undefined || allocation.initializationLeaseId !== null)
|
|
131
|
+
return false;
|
|
132
|
+
// Read the key after acquiring the allocation lock, so a completion that
|
|
133
|
+
// committed while we waited is visible even if its response was lost.
|
|
134
|
+
const [anchor] = await tx
|
|
135
|
+
.select({ publicKey: workflowRun.publicKey })
|
|
136
|
+
.from(workflowRun)
|
|
137
|
+
.where(eq(workflowRun.id, allocation.anchorRunId));
|
|
138
|
+
return anchor !== undefined && anchor.publicKey !== null;
|
|
139
|
+
}
|
|
65
140
|
async function insertSidecarIdentity(tx, args) {
|
|
66
141
|
await tx.insert(sidecar).values({
|
|
67
142
|
id: args.sidecarId,
|
|
68
143
|
url: null,
|
|
69
144
|
tokenHashSha256: args.tokenHashSha256,
|
|
70
|
-
credentialScope: "allocated",
|
|
71
145
|
status: "offline",
|
|
72
146
|
createdAt: args.now,
|
|
73
147
|
updatedAt: args.now,
|
|
@@ -96,6 +170,7 @@ export function createSidecarAllocationStore(db) {
|
|
|
96
170
|
.set({
|
|
97
171
|
status: "releasing",
|
|
98
172
|
generation: args.expectedGeneration + 1,
|
|
173
|
+
initializationLeaseId: null,
|
|
99
174
|
nextAttemptAt: now,
|
|
100
175
|
reconciliationLeaseId: null,
|
|
101
176
|
reconciliationLeaseExpiresAt: null,
|
|
@@ -108,11 +183,59 @@ export function createSidecarAllocationStore(db) {
|
|
|
108
183
|
: {}),
|
|
109
184
|
updatedAt: now,
|
|
110
185
|
})
|
|
111
|
-
.where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.status, args.expectedStatus), eq(sidecarAllocation.generation, args.expectedGeneration), ...leaseCondition(args.expectedLeaseId)
|
|
186
|
+
.where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.status, args.expectedStatus), eq(sidecarAllocation.generation, args.expectedGeneration), ...leaseCondition(args.expectedLeaseId), ...(args.expectedInitializationLeaseId !== undefined
|
|
187
|
+
? [
|
|
188
|
+
eq(sidecarAllocation.initializationLeaseId, args.expectedInitializationLeaseId),
|
|
189
|
+
]
|
|
190
|
+
: [])))
|
|
112
191
|
.returning();
|
|
113
192
|
return updated === undefined ? null : parseSidecarAllocationRow(updated);
|
|
114
193
|
}
|
|
115
194
|
return {
|
|
195
|
+
beginInitialization(args) {
|
|
196
|
+
return writeInitialization(args);
|
|
197
|
+
},
|
|
198
|
+
async completeInitialization(args) {
|
|
199
|
+
return (await writeInitialization(args, args)) !== null;
|
|
200
|
+
},
|
|
201
|
+
async clearUnsentInitialization(args) {
|
|
202
|
+
// No signal gate: the sole caller invokes this only for a proven-unsent
|
|
203
|
+
// frame, which is reachable precisely when the attempt was cancelled.
|
|
204
|
+
// The WHERE clause is the guard. Marker equality proves no newer
|
|
205
|
+
// attempt began, and generation/status equality proves the allocation
|
|
206
|
+
// did not move on. The reconciliation lease is deliberately not
|
|
207
|
+
// required: a disconnect nulls the lease while leaving this attempt's
|
|
208
|
+
// marker behind, and only this attempt's own clear may remove it. Restore
|
|
209
|
+
// its previous key in the same transaction so absence of the marker cannot
|
|
210
|
+
// expose a live workflow as keyless or certify a different attempt.
|
|
211
|
+
return db.transaction(async (tx) => {
|
|
212
|
+
const condition = initializationConditions(args, args.leaseId, {
|
|
213
|
+
requireCurrentLease: false,
|
|
214
|
+
});
|
|
215
|
+
const [allocation] = await tx
|
|
216
|
+
.select({ id: sidecarAllocation.id })
|
|
217
|
+
.from(sidecarAllocation)
|
|
218
|
+
.where(condition)
|
|
219
|
+
.for("update");
|
|
220
|
+
if (allocation === undefined)
|
|
221
|
+
return false;
|
|
222
|
+
const [anchor] = await tx
|
|
223
|
+
.update(workflowRun)
|
|
224
|
+
.set({ publicKey: args.previousPublicKey })
|
|
225
|
+
.where(and(eq(workflowRun.id, args.anchorRunId), eq(workflowRun.anchorRunId, args.anchorRunId), eq(workflowRun.tenantId, args.tenantId)))
|
|
226
|
+
.returning({ id: workflowRun.id });
|
|
227
|
+
if (anchor === undefined)
|
|
228
|
+
throw new Error("Initialization anchor is missing");
|
|
229
|
+
const [updated] = await tx
|
|
230
|
+
.update(sidecarAllocation)
|
|
231
|
+
.set({ initializationLeaseId: null })
|
|
232
|
+
.where(condition)
|
|
233
|
+
.returning({ id: sidecarAllocation.id });
|
|
234
|
+
if (updated === undefined)
|
|
235
|
+
throw new Error("Initialization attempt changed before rollback");
|
|
236
|
+
return true;
|
|
237
|
+
});
|
|
238
|
+
},
|
|
116
239
|
async createPending(args, tx) {
|
|
117
240
|
const executor = tx ?? db;
|
|
118
241
|
const [anchor] = await executor
|
|
@@ -144,7 +267,6 @@ export function createSidecarAllocationStore(db) {
|
|
|
144
267
|
if (args.provisionerApiVersion !== 1) {
|
|
145
268
|
throw new Error("sidecarAllocationStore.createPending: unsupported API version");
|
|
146
269
|
}
|
|
147
|
-
const placement = SidecarPlacementRequirement.assert(args.placement ?? { sharing: "exclusive", reuse: "never" });
|
|
148
270
|
const now = databaseTimestamp(args.now);
|
|
149
271
|
const [inserted] = await executor
|
|
150
272
|
.insert(sidecarAllocation)
|
|
@@ -155,8 +277,6 @@ export function createSidecarAllocationStore(db) {
|
|
|
155
277
|
provisionerId: args.provisionerId,
|
|
156
278
|
provisionerApiVersion: args.provisionerApiVersion,
|
|
157
279
|
provisionerBindingFingerprint: args.provisionerBindingFingerprint,
|
|
158
|
-
placementSharing: placement.sharing,
|
|
159
|
-
sidecarReuse: placement.reuse ?? "never",
|
|
160
280
|
status: "pending",
|
|
161
281
|
generation: 0,
|
|
162
282
|
nextAttemptAt: now,
|
|
@@ -169,12 +289,41 @@ export function createSidecarAllocationStore(db) {
|
|
|
169
289
|
}
|
|
170
290
|
return parseSidecarAllocationRow(inserted);
|
|
171
291
|
},
|
|
292
|
+
async createAdopted(args, tx) {
|
|
293
|
+
const createdAt = databaseTimestamp(args.now);
|
|
294
|
+
const [inserted] = await (tx ?? db)
|
|
295
|
+
.insert(sidecarAllocation)
|
|
296
|
+
.values({
|
|
297
|
+
id: args.id,
|
|
298
|
+
anchorRunId: args.anchorRunId,
|
|
299
|
+
tenantId: args.tenantId,
|
|
300
|
+
provisionerId: args.provisionerId,
|
|
301
|
+
provisionerApiVersion: args.provisionerApiVersion,
|
|
302
|
+
provisionerBindingFingerprint: args.provisionerBindingFingerprint,
|
|
303
|
+
sidecarId: args.sidecarId,
|
|
304
|
+
status: "allocated",
|
|
305
|
+
generation: args.generation,
|
|
306
|
+
ensureAcceptedGeneration: args.generation,
|
|
307
|
+
...(args.externalRef !== undefined
|
|
308
|
+
? { externalRef: args.externalRef }
|
|
309
|
+
: {}),
|
|
310
|
+
connectDeadline: args.connectDeadline,
|
|
311
|
+
nextAttemptAt: createdAt,
|
|
312
|
+
createdAt,
|
|
313
|
+
updatedAt: createdAt,
|
|
314
|
+
})
|
|
315
|
+
.returning();
|
|
316
|
+
if (inserted === undefined) {
|
|
317
|
+
throw new Error(`sidecarAllocationStore.createAdopted: insert returned no row for ${args.id}`);
|
|
318
|
+
}
|
|
319
|
+
return parseSidecarAllocationRow(inserted);
|
|
320
|
+
},
|
|
172
321
|
async bindInitialSidecar(args) {
|
|
173
322
|
return db.transaction(async (tx) => {
|
|
174
323
|
const [allocation] = await tx
|
|
175
324
|
.select()
|
|
176
325
|
.from(sidecarAllocation)
|
|
177
|
-
.where(eq(sidecarAllocation.id, args.allocationId))
|
|
326
|
+
.where(and(eq(sidecarAllocation.id, args.allocationId), ...leaseCondition(args.expectedLeaseId)))
|
|
178
327
|
.limit(1)
|
|
179
328
|
.for("update");
|
|
180
329
|
if (allocation === undefined ||
|
|
@@ -217,7 +366,7 @@ export function createSidecarAllocationStore(db) {
|
|
|
217
366
|
const [allocation] = await tx
|
|
218
367
|
.select()
|
|
219
368
|
.from(sidecarAllocation)
|
|
220
|
-
.where(eq(sidecarAllocation.id, args.allocationId))
|
|
369
|
+
.where(and(eq(sidecarAllocation.id, args.allocationId), ...leaseCondition(args.expectedLeaseId)))
|
|
221
370
|
.limit(1)
|
|
222
371
|
.for("update");
|
|
223
372
|
if (allocation === undefined ||
|
|
@@ -300,11 +449,15 @@ export function createSidecarAllocationStore(db) {
|
|
|
300
449
|
async beginReplacement(args) {
|
|
301
450
|
const now = databaseTimestamp(args.now);
|
|
302
451
|
return db.transaction(async (tx) => {
|
|
452
|
+
if (args.onlyIfInitializationIncomplete &&
|
|
453
|
+
(await initializationCompleted(tx, args)))
|
|
454
|
+
return null;
|
|
303
455
|
const [updated] = await tx
|
|
304
456
|
.update(sidecarAllocation)
|
|
305
457
|
.set({
|
|
306
458
|
status: "replacing",
|
|
307
459
|
generation: args.expectedGeneration + 1,
|
|
460
|
+
initializationLeaseId: null,
|
|
308
461
|
ensureAcceptedGeneration: null,
|
|
309
462
|
nextAttemptAt: args.nextAttemptAt,
|
|
310
463
|
reconciliationLeaseId: null,
|
|
@@ -314,7 +467,11 @@ export function createSidecarAllocationStore(db) {
|
|
|
314
467
|
failureMessage: args.failureMessage,
|
|
315
468
|
updatedAt: now,
|
|
316
469
|
})
|
|
317
|
-
.where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.status, args.expectedStatus), eq(sidecarAllocation.generation, args.expectedGeneration), ...leaseCondition(args.expectedLeaseId)
|
|
470
|
+
.where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.status, args.expectedStatus), eq(sidecarAllocation.generation, args.expectedGeneration), ...leaseCondition(args.expectedLeaseId), ...(args.expectedInitializationLeaseId !== undefined
|
|
471
|
+
? [
|
|
472
|
+
eq(sidecarAllocation.initializationLeaseId, args.expectedInitializationLeaseId),
|
|
473
|
+
]
|
|
474
|
+
: [])))
|
|
318
475
|
.returning();
|
|
319
476
|
if (updated === undefined)
|
|
320
477
|
return null;
|
|
@@ -333,6 +490,9 @@ export function createSidecarAllocationStore(db) {
|
|
|
333
490
|
async beginUnrecoverableRelease(args) {
|
|
334
491
|
const now = databaseTimestamp(args.now);
|
|
335
492
|
return db.transaction(async (tx) => {
|
|
493
|
+
if (args.onlyIfInitializationIncomplete &&
|
|
494
|
+
(await initializationCompleted(tx, args)))
|
|
495
|
+
return null;
|
|
336
496
|
const releasing = await beginRelease({
|
|
337
497
|
...args,
|
|
338
498
|
expectedStatus: "allocated",
|
|
@@ -360,6 +520,31 @@ export function createSidecarAllocationStore(db) {
|
|
|
360
520
|
.returning();
|
|
361
521
|
return updated === undefined ? null : parseSidecarAllocationRow(updated);
|
|
362
522
|
},
|
|
523
|
+
async markDestroyFailed(args) {
|
|
524
|
+
const now = databaseTimestamp(args.now);
|
|
525
|
+
return db.transaction(async (tx) => {
|
|
526
|
+
const [updated] = await tx
|
|
527
|
+
.update(sidecarAllocation)
|
|
528
|
+
.set({
|
|
529
|
+
status: "destroy_failed",
|
|
530
|
+
failureCode: args.code,
|
|
531
|
+
failureMessage: args.message,
|
|
532
|
+
nextAttemptAt: null,
|
|
533
|
+
reconciliationLeaseId: null,
|
|
534
|
+
reconciliationLeaseExpiresAt: null,
|
|
535
|
+
connectDeadline: null,
|
|
536
|
+
destroyAttempts: sql `${sidecarAllocation.destroyAttempts} + 1`,
|
|
537
|
+
updatedAt: now,
|
|
538
|
+
})
|
|
539
|
+
.where(and(eq(sidecarAllocation.id, args.allocationId), inArray(sidecarAllocation.status, ["replacing", "releasing"]), eq(sidecarAllocation.generation, args.expectedGeneration), ...leaseCondition(args.expectedLeaseId)))
|
|
540
|
+
.returning();
|
|
541
|
+
if (updated === undefined)
|
|
542
|
+
return null;
|
|
543
|
+
await failRunningRuns(tx, updated.anchorRunId, now);
|
|
544
|
+
await workflowRunDispatchStore.failUnsettled(updated.anchorRunId, args.code, args.message, now, tx);
|
|
545
|
+
return parseSidecarAllocationRow(updated);
|
|
546
|
+
});
|
|
547
|
+
},
|
|
363
548
|
async failWithoutInfrastructure(args, tx) {
|
|
364
549
|
const now = databaseTimestamp(args.now);
|
|
365
550
|
const fail = async (executor) => {
|
|
@@ -402,7 +587,14 @@ export function createSidecarAllocationStore(db) {
|
|
|
402
587
|
const [candidate] = await tx
|
|
403
588
|
.select({ id: sidecarAllocation.id })
|
|
404
589
|
.from(sidecarAllocation)
|
|
405
|
-
.where(and(inArray(sidecarAllocation.status, activeStatuses), lte(sidecarAllocation.nextAttemptAt, sql `now()`),
|
|
590
|
+
.where(and(inArray(sidecarAllocation.status, activeStatuses), lte(sidecarAllocation.nextAttemptAt, sql `now()`), ...(args.excludedAllocationIds !== undefined &&
|
|
591
|
+
args.excludedAllocationIds.length > 0
|
|
592
|
+
? [
|
|
593
|
+
notInArray(sidecarAllocation.id, [
|
|
594
|
+
...args.excludedAllocationIds,
|
|
595
|
+
]),
|
|
596
|
+
]
|
|
597
|
+
: []), or(isNull(sidecarAllocation.reconciliationLeaseExpiresAt), lte(sidecarAllocation.reconciliationLeaseExpiresAt, sql `now()`))))
|
|
406
598
|
.orderBy(asc(sidecarAllocation.nextAttemptAt), asc(sidecarAllocation.createdAt))
|
|
407
599
|
.limit(1)
|
|
408
600
|
.for("update", { skipLocked: true });
|
|
@@ -425,12 +617,20 @@ export function createSidecarAllocationStore(db) {
|
|
|
425
617
|
const [updated] = await db
|
|
426
618
|
.update(sidecarAllocation)
|
|
427
619
|
.set({
|
|
428
|
-
reconciliationLeaseExpiresAt: sql `
|
|
620
|
+
reconciliationLeaseExpiresAt: sql `clock_timestamp() + (${leaseDurationMs} * interval '1 millisecond')`,
|
|
429
621
|
})
|
|
430
|
-
.where(and(eq(sidecarAllocation.id, allocationId),
|
|
622
|
+
.where(and(eq(sidecarAllocation.id, allocationId), ...leaseCondition(leaseId)))
|
|
431
623
|
.returning({ id: sidecarAllocation.id });
|
|
432
624
|
return updated !== undefined;
|
|
433
625
|
},
|
|
626
|
+
async isReconciliationLeaseCurrent(allocationId, generation, leaseId) {
|
|
627
|
+
const [allocation] = await db
|
|
628
|
+
.select({ id: sidecarAllocation.id })
|
|
629
|
+
.from(sidecarAllocation)
|
|
630
|
+
.where(and(eq(sidecarAllocation.id, allocationId), eq(sidecarAllocation.generation, generation), ...leaseCondition(leaseId)))
|
|
631
|
+
.limit(1);
|
|
632
|
+
return allocation !== undefined;
|
|
633
|
+
},
|
|
434
634
|
async markConnectionReady(args) {
|
|
435
635
|
const [updated] = await db
|
|
436
636
|
.update(sidecarAllocation)
|
|
@@ -443,7 +643,7 @@ export function createSidecarAllocationStore(db) {
|
|
|
443
643
|
failureMessage: null,
|
|
444
644
|
updatedAt: databaseTimestamp(args.now),
|
|
445
645
|
})
|
|
446
|
-
.where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.status, "allocated"), eq(sidecarAllocation.generation, args.generation), eq(sidecarAllocation.ensureAcceptedGeneration, args.generation), ...leaseCondition(args.expectedLeaseId)))
|
|
646
|
+
.where(and(eq(sidecarAllocation.id, args.allocationId), eq(sidecarAllocation.status, "allocated"), eq(sidecarAllocation.generation, args.generation), eq(sidecarAllocation.ensureAcceptedGeneration, args.generation), isNull(sidecarAllocation.initializationLeaseId), ...leaseCondition(args.expectedLeaseId)))
|
|
447
647
|
.returning();
|
|
448
648
|
return updated === undefined ? null : parseSidecarAllocationRow(updated);
|
|
449
649
|
},
|
|
@@ -489,14 +689,16 @@ export function createSidecarAllocationStore(db) {
|
|
|
489
689
|
reconciliationLeaseId: null,
|
|
490
690
|
reconciliationLeaseExpiresAt: null,
|
|
491
691
|
})
|
|
492
|
-
.where(and(eq(sidecarAllocation.id, allocationId),
|
|
692
|
+
.where(and(eq(sidecarAllocation.id, allocationId), ...leaseCondition(leaseId)))
|
|
493
693
|
.returning({ id: sidecarAllocation.id });
|
|
494
694
|
return updated !== undefined;
|
|
495
695
|
},
|
|
496
696
|
async wakeReconciliation(allocationId, generation) {
|
|
497
697
|
const [updated] = await db
|
|
498
698
|
.update(sidecarAllocation)
|
|
499
|
-
.set({
|
|
699
|
+
.set({
|
|
700
|
+
nextAttemptAt: sql `now()`,
|
|
701
|
+
})
|
|
500
702
|
.where(and(eq(sidecarAllocation.id, allocationId), eq(sidecarAllocation.generation, generation), inArray(sidecarAllocation.status, activeStatuses)))
|
|
501
703
|
.returning({ id: sidecarAllocation.id });
|
|
502
704
|
return updated !== undefined;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LocalPrincipalSigner } from "@intx/types";
|
|
2
|
+
import type { DBExecutor } from "./client.js";
|
|
3
|
+
import type { PrincipalKeyStore } from "./principal-key-store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a principal's trusted signer identity from the hub's own key store.
|
|
6
|
+
*
|
|
7
|
+
* The returned `publicKey` is the principal's active key as the hub holds it, so
|
|
8
|
+
* a verifier can check a signature against it without trusting any key on the
|
|
9
|
+
* wire. Throws when the principal has no active key -- an unresolvable signer
|
|
10
|
+
* must fail loudly rather than default.
|
|
11
|
+
*/
|
|
12
|
+
export declare function lookupLocalPrincipalSigner(principalKeyStore: PrincipalKeyStore, principalId: string, tx?: DBExecutor): Promise<LocalPrincipalSigner>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a principal's trusted signer identity from the hub's own key store.
|
|
3
|
+
*
|
|
4
|
+
* The returned `publicKey` is the principal's active key as the hub holds it, so
|
|
5
|
+
* a verifier can check a signature against it without trusting any key on the
|
|
6
|
+
* wire. Throws when the principal has no active key -- an unresolvable signer
|
|
7
|
+
* must fail loudly rather than default.
|
|
8
|
+
*/
|
|
9
|
+
export async function lookupLocalPrincipalSigner(principalKeyStore, principalId, tx) {
|
|
10
|
+
return {
|
|
11
|
+
kind: "local-principal",
|
|
12
|
+
principalId,
|
|
13
|
+
publicKey: await principalKeyStore.getPublicKey(principalId, tx),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TenantSidecarCapabilityPolicy } from "@intx/types";
|
|
1
2
|
import type { DB } from "./client.js";
|
|
2
3
|
/**
|
|
3
4
|
* Walks the tenant parentId chain from the given tenant up to the root.
|
|
@@ -7,6 +8,7 @@ import type { DB } from "./client.js";
|
|
|
7
8
|
* chain that could omit inherited policy or configuration.
|
|
8
9
|
*/
|
|
9
10
|
export declare function getAncestorChain(db: DB["db"], tenantId: string): Promise<string[]>;
|
|
11
|
+
export declare function resolveTenantSidecarCapabilityPolicies(db: DB["db"], tenantId: string): Promise<readonly TenantSidecarCapabilityPolicy[]>;
|
|
10
12
|
/**
|
|
11
13
|
* Returns the tenant and every distinct tenant in its subtree (children,
|
|
12
14
|
* their children, and so on), discovered breadth-first. Used to find every
|
package/dist/tenant-hierarchy.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
1
2
|
import { eq, inArray } from "drizzle-orm";
|
|
3
|
+
import { TenantConfig } from "@intx/types";
|
|
2
4
|
import { tenant } from "./schema/tenants.js";
|
|
3
5
|
/**
|
|
4
6
|
* Walks the tenant parentId chain from the given tenant up to the root.
|
|
@@ -27,6 +29,31 @@ export async function getAncestorChain(db, tenantId) {
|
|
|
27
29
|
}
|
|
28
30
|
return chain;
|
|
29
31
|
}
|
|
32
|
+
export async function resolveTenantSidecarCapabilityPolicies(db, tenantId) {
|
|
33
|
+
const chain = await getAncestorChain(db, tenantId);
|
|
34
|
+
const rows = await db.query.tenant.findMany({
|
|
35
|
+
where: inArray(tenant.id, chain),
|
|
36
|
+
columns: { id: true, config: true },
|
|
37
|
+
});
|
|
38
|
+
const rowsById = new Map(rows.map((row) => [row.id, row]));
|
|
39
|
+
const policies = [];
|
|
40
|
+
for (const ancestorId of chain) {
|
|
41
|
+
const row = rowsById.get(ancestorId);
|
|
42
|
+
if (row === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
if (row.config === null)
|
|
45
|
+
continue;
|
|
46
|
+
const config = TenantConfig(row.config);
|
|
47
|
+
if (config instanceof type.errors) {
|
|
48
|
+
throw new Error(`Tenant ${row.id} has invalid configuration: ${config.summary}`);
|
|
49
|
+
}
|
|
50
|
+
const rules = config.sidecarPlacement?.capabilities;
|
|
51
|
+
if (rules !== undefined && rules.length > 0) {
|
|
52
|
+
policies.push({ tenantId: row.id, rules });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return policies;
|
|
56
|
+
}
|
|
30
57
|
/**
|
|
31
58
|
* Returns the tenant and every distinct tenant in its subtree (children,
|
|
32
59
|
* their children, and so on), discovered breadth-first. Used to find every
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { WorkflowProbeResultFrame } from "@intx/types/sidecar";
|
|
2
|
+
import type { WorkflowDefinitionSource } from "@intx/types/workflow-sources";
|
|
3
|
+
import type { DB, DBExecutor } from "./client.js";
|
|
4
|
+
import { workflowProbe, type WorkflowProbeStatus } from "./schema/index.js";
|
|
5
|
+
type DBHandle = DB["db"];
|
|
6
|
+
type WorkflowProbeResult = Omit<WorkflowProbeResultFrame, "type" | "requestId">;
|
|
7
|
+
type WorkflowProbeRow = typeof workflowProbe.$inferSelect;
|
|
8
|
+
export type WorkflowProbe = WorkflowProbeRow;
|
|
9
|
+
export type CreateWorkflowProbeArgs = {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly tenantId: string;
|
|
12
|
+
readonly definitionAssetId: string;
|
|
13
|
+
readonly source: WorkflowDefinitionSource;
|
|
14
|
+
readonly entry: string;
|
|
15
|
+
readonly pin?: string;
|
|
16
|
+
readonly provisionerId: string;
|
|
17
|
+
readonly provisionerApiVersion: 1;
|
|
18
|
+
readonly provisionerBindingFingerprint: string;
|
|
19
|
+
readonly now?: Date;
|
|
20
|
+
};
|
|
21
|
+
export type BindWorkflowProbeSidecarArgs = {
|
|
22
|
+
readonly probeId: string;
|
|
23
|
+
readonly sidecarId: string;
|
|
24
|
+
readonly tokenHashSha256: Uint8Array;
|
|
25
|
+
readonly now?: Date;
|
|
26
|
+
};
|
|
27
|
+
export declare function createWorkflowProbeStore(db: DBHandle): {
|
|
28
|
+
create(args: CreateWorkflowProbeArgs, tx?: DBExecutor): Promise<WorkflowProbe>;
|
|
29
|
+
bindSidecar(args: BindWorkflowProbeSidecarArgs, tx?: DBExecutor): Promise<WorkflowProbe | null>;
|
|
30
|
+
markProbing(args: {
|
|
31
|
+
probeId: string;
|
|
32
|
+
externalRef?: string;
|
|
33
|
+
now?: Date;
|
|
34
|
+
}): Promise<WorkflowProbe | null>;
|
|
35
|
+
recordResult(probeId: string, result: WorkflowProbeResult, now?: Date): Promise<WorkflowProbe | null>;
|
|
36
|
+
transition(probeId: string, from: readonly WorkflowProbeStatus[], to: WorkflowProbeStatus, opts?: {
|
|
37
|
+
failureCode?: string;
|
|
38
|
+
failureMessage?: string;
|
|
39
|
+
now?: Date;
|
|
40
|
+
tx?: DBExecutor;
|
|
41
|
+
}): Promise<WorkflowProbe | null>;
|
|
42
|
+
get(probeId: string): Promise<WorkflowProbe | null>;
|
|
43
|
+
listActive(): Promise<WorkflowProbe[]>;
|
|
44
|
+
listReleasing(): Promise<WorkflowProbe[]>;
|
|
45
|
+
};
|
|
46
|
+
export type WorkflowProbeStore = ReturnType<typeof createWorkflowProbeStore>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
2
|
+
import { sidecar, workflowProbe } from "./schema/index.js";
|
|
3
|
+
const activeStatuses = [
|
|
4
|
+
"pending",
|
|
5
|
+
"provisioning",
|
|
6
|
+
"probing",
|
|
7
|
+
"releasing",
|
|
8
|
+
];
|
|
9
|
+
function timestamp(now) {
|
|
10
|
+
return now ?? sql `now()`;
|
|
11
|
+
}
|
|
12
|
+
export function createWorkflowProbeStore(db) {
|
|
13
|
+
return {
|
|
14
|
+
async create(args, tx) {
|
|
15
|
+
const createdAt = timestamp(args.now);
|
|
16
|
+
const [created] = await (tx ?? db)
|
|
17
|
+
.insert(workflowProbe)
|
|
18
|
+
.values({
|
|
19
|
+
id: args.id,
|
|
20
|
+
tenantId: args.tenantId,
|
|
21
|
+
definitionAssetId: args.definitionAssetId,
|
|
22
|
+
source: args.source,
|
|
23
|
+
entry: args.entry,
|
|
24
|
+
...(args.pin !== undefined ? { pin: args.pin } : {}),
|
|
25
|
+
provisionerId: args.provisionerId,
|
|
26
|
+
provisionerApiVersion: args.provisionerApiVersion,
|
|
27
|
+
provisionerBindingFingerprint: args.provisionerBindingFingerprint,
|
|
28
|
+
createdAt,
|
|
29
|
+
updatedAt: createdAt,
|
|
30
|
+
})
|
|
31
|
+
.returning();
|
|
32
|
+
if (created === undefined) {
|
|
33
|
+
throw new Error(`Failed to create workflow probe ${args.id}`);
|
|
34
|
+
}
|
|
35
|
+
return created;
|
|
36
|
+
},
|
|
37
|
+
async bindSidecar(args, tx) {
|
|
38
|
+
const bind = async (executor) => {
|
|
39
|
+
const updatedAt = timestamp(args.now);
|
|
40
|
+
const [probe] = await executor
|
|
41
|
+
.select({ status: workflowProbe.status })
|
|
42
|
+
.from(workflowProbe)
|
|
43
|
+
.where(eq(workflowProbe.id, args.probeId))
|
|
44
|
+
.limit(1)
|
|
45
|
+
.for("update");
|
|
46
|
+
if (probe?.status !== "pending")
|
|
47
|
+
return null;
|
|
48
|
+
await executor.insert(sidecar).values({
|
|
49
|
+
id: args.sidecarId,
|
|
50
|
+
url: null,
|
|
51
|
+
tokenHashSha256: args.tokenHashSha256,
|
|
52
|
+
status: "offline",
|
|
53
|
+
createdAt: updatedAt,
|
|
54
|
+
updatedAt,
|
|
55
|
+
});
|
|
56
|
+
const [updated] = await executor
|
|
57
|
+
.update(workflowProbe)
|
|
58
|
+
.set({
|
|
59
|
+
sidecarId: args.sidecarId,
|
|
60
|
+
status: "provisioning",
|
|
61
|
+
updatedAt,
|
|
62
|
+
})
|
|
63
|
+
.where(eq(workflowProbe.id, args.probeId))
|
|
64
|
+
.returning();
|
|
65
|
+
return updated ?? null;
|
|
66
|
+
};
|
|
67
|
+
return tx === undefined ? db.transaction(bind) : bind(tx);
|
|
68
|
+
},
|
|
69
|
+
async markProbing(args) {
|
|
70
|
+
const [updated] = await db
|
|
71
|
+
.update(workflowProbe)
|
|
72
|
+
.set({
|
|
73
|
+
status: "probing",
|
|
74
|
+
...(args.externalRef !== undefined
|
|
75
|
+
? { externalRef: args.externalRef }
|
|
76
|
+
: {}),
|
|
77
|
+
updatedAt: timestamp(args.now),
|
|
78
|
+
})
|
|
79
|
+
.where(and(eq(workflowProbe.id, args.probeId), eq(workflowProbe.status, "provisioning")))
|
|
80
|
+
.returning();
|
|
81
|
+
return updated ?? null;
|
|
82
|
+
},
|
|
83
|
+
async recordResult(probeId, result, now) {
|
|
84
|
+
const [updated] = await db
|
|
85
|
+
.update(workflowProbe)
|
|
86
|
+
.set({ result, updatedAt: timestamp(now) })
|
|
87
|
+
.where(and(eq(workflowProbe.id, probeId), eq(workflowProbe.status, "probing")))
|
|
88
|
+
.returning();
|
|
89
|
+
return updated ?? null;
|
|
90
|
+
},
|
|
91
|
+
async transition(probeId, from, to, opts) {
|
|
92
|
+
const [updated] = await (opts?.tx ?? db)
|
|
93
|
+
.update(workflowProbe)
|
|
94
|
+
.set({
|
|
95
|
+
status: to,
|
|
96
|
+
...(opts?.failureCode !== undefined
|
|
97
|
+
? { failureCode: opts.failureCode }
|
|
98
|
+
: {}),
|
|
99
|
+
...(opts?.failureMessage !== undefined
|
|
100
|
+
? { failureMessage: opts.failureMessage }
|
|
101
|
+
: {}),
|
|
102
|
+
updatedAt: timestamp(opts?.now),
|
|
103
|
+
})
|
|
104
|
+
.where(and(eq(workflowProbe.id, probeId), inArray(workflowProbe.status, [...from])))
|
|
105
|
+
.returning();
|
|
106
|
+
return updated ?? null;
|
|
107
|
+
},
|
|
108
|
+
async get(probeId) {
|
|
109
|
+
return ((await db.query.workflowProbe.findFirst({
|
|
110
|
+
where: eq(workflowProbe.id, probeId),
|
|
111
|
+
})) ?? null);
|
|
112
|
+
},
|
|
113
|
+
async listActive() {
|
|
114
|
+
return db.query.workflowProbe.findMany({
|
|
115
|
+
where: inArray(workflowProbe.status, [...activeStatuses]),
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
async listReleasing() {
|
|
119
|
+
return db.query.workflowProbe.findMany({
|
|
120
|
+
where: eq(workflowProbe.status, "releasing"),
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|