@evident-ai/runner-cdk 3.5.2-dev.a72611d → 3.5.2-dev.bbd799b
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 +91 -40
- package/dist/controller-lambda/handler.js +74 -79
- package/dist/evident-scale-to-zero-construct.js +7 -0
- package/dist/image-version-pruner-lambda/handler.js +28250 -0
- package/dist/image-version-reporter-lambda/handler.js +15 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/microvm/constants.d.ts +1 -1
- package/dist/microvm/constants.js +7 -4
- package/dist/microvm/construct.d.ts +12 -8
- package/dist/microvm/construct.js +42 -33
- package/dist/microvm/controller/{doorbell.d.ts → doorbell-request.d.ts} +6 -6
- package/dist/microvm/controller/{doorbell.js → doorbell-request.js} +2 -2
- package/dist/microvm/controller/handle-doorbell.d.ts +2 -2
- package/dist/microvm/controller/handle-doorbell.js +62 -62
- package/dist/microvm/image/stage-context.d.ts +13 -38
- package/dist/microvm/image/stage-context.js +31 -118
- package/dist/microvm/image-version-pruner/construct.d.ts +13 -0
- package/dist/microvm/image-version-pruner/construct.js +79 -0
- package/dist/microvm/image-version-pruner/handler.d.ts +20 -0
- package/dist/microvm/image-version-pruner/handler.js +116 -0
- package/dist/microvm/image-version-reporter/construct.d.ts +1 -1
- package/dist/microvm/image-version-reporter/construct.js +3 -3
- package/dist/microvm/image-version-reporter/handler.d.ts +2 -2
- package/dist/microvm/image-version-reporter/handler.js +9 -7
- package/dist/microvm/shapes.d.ts +10 -21
- package/dist/microvm/shapes.js +10 -17
- package/dist/waker-lambda/handler.js +3 -3
- package/package.json +3 -3
- package/dist/microvm-image-context/Dockerfile +0 -190
- package/dist/microvm-image-context/hook-server.js +0 -286
- package/dist/microvm-image-context/hooks/common.sh +0 -905
- package/dist/microvm-image-context/hooks/resume +0 -81
- package/dist/microvm-image-context/hooks/run +0 -95
- package/dist/microvm-image-context/hooks/suspend +0 -21
- package/dist/microvm-image-context/hooks/terminate +0 -36
|
@@ -4,7 +4,7 @@ exports.handleDoorbell = handleDoorbell;
|
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
5
|
const sdk_1 = require("@evident/sdk");
|
|
6
6
|
const constants_1 = require("../constants");
|
|
7
|
-
const
|
|
7
|
+
const doorbell_request_1 = require("./doorbell-request");
|
|
8
8
|
const throttle_retry_1 = require("./throttle-retry");
|
|
9
9
|
const REASON = {
|
|
10
10
|
invalidSignature: 'invalid_signature',
|
|
@@ -65,7 +65,7 @@ function decide({ statusCode, action, reason, state, runnerId, microvmId, starte
|
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* `RunMicrovm`'s idempotency key. Keyed on `runner_id` plus a "generation"
|
|
68
|
-
* marker: the dead VM's `microvm_id` on the recovery arms (`
|
|
68
|
+
* marker: the dead VM's `microvm_id` on the recovery arms (`doorbellRequest.microvmId`
|
|
69
69
|
* is defined — TERMINATED/TERMINATING/not_found and the resume-failed
|
|
70
70
|
* fallback), else `occurred_at` on the cold arm (no `microvm_id` yet). The id
|
|
71
71
|
* advances every VM generation — `mvm-A` dies, the token keyed on it starts
|
|
@@ -92,9 +92,9 @@ function decide({ statusCode, action, reason, state, runnerId, microvmId, starte
|
|
|
92
92
|
* that retried the same event with a different shape starts TWO concurrent
|
|
93
93
|
* 8-hour VMs instead of deduping to one.
|
|
94
94
|
*/
|
|
95
|
-
function clientTokenFor(
|
|
96
|
-
const generation =
|
|
97
|
-
return (0, node_crypto_1.createHash)('sha256').update(`${
|
|
95
|
+
function clientTokenFor(doorbellRequest) {
|
|
96
|
+
const generation = doorbellRequest.microvmId ?? doorbellRequest.occurredAt;
|
|
97
|
+
return (0, node_crypto_1.createHash)('sha256').update(`${doorbellRequest.runnerId}:${generation}`).digest('hex');
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
100
100
|
* MicroVM image versions are dotted decimals (`48.0`, `50.0`), so they must NOT
|
|
@@ -140,15 +140,15 @@ function compareImageVersions(a, b) {
|
|
|
140
140
|
* unparseable all warn and return false, i.e. resume exactly as before
|
|
141
141
|
* (`development-workflow.mdc`: never fail a gate on absent evidence).
|
|
142
142
|
*/
|
|
143
|
-
async function shouldRecreateForNewerImage(
|
|
143
|
+
async function shouldRecreateForNewerImage(doorbellRequest, shape, runningVersion, microvm) {
|
|
144
144
|
if (runningVersion === undefined) {
|
|
145
|
-
console.warn(`[doorbell] GetMicrovm reported no imageVersion for runner ${
|
|
145
|
+
console.warn(`[doorbell] GetMicrovm reported no imageVersion for runner ${doorbellRequest.runnerId}; resuming`);
|
|
146
146
|
return false;
|
|
147
147
|
}
|
|
148
148
|
const running = parseImageVersion(runningVersion);
|
|
149
149
|
if (running === undefined) {
|
|
150
150
|
console.warn(`[doorbell] unparseable running imageVersion "${runningVersion}" for runner ` +
|
|
151
|
-
`${
|
|
151
|
+
`${doorbellRequest.runnerId}; resuming`);
|
|
152
152
|
return false;
|
|
153
153
|
}
|
|
154
154
|
let latestVersion;
|
|
@@ -159,14 +159,14 @@ async function shouldRecreateForNewerImage(doorbell, shape, runningVersion, micr
|
|
|
159
159
|
latestVersion = await microvm.latestImageVersion(shape.imageArn);
|
|
160
160
|
}
|
|
161
161
|
catch (error) {
|
|
162
|
-
console.warn(`[doorbell] GetMicrovmImage failed for runner ${
|
|
162
|
+
console.warn(`[doorbell] GetMicrovmImage failed for runner ${doorbellRequest.runnerId}; resuming on the ` +
|
|
163
163
|
`running image ${runningVersion}: ${error instanceof Error ? error.message : String(error)}`);
|
|
164
164
|
return false;
|
|
165
165
|
}
|
|
166
166
|
const latest = latestVersion === undefined ? undefined : parseImageVersion(latestVersion);
|
|
167
167
|
if (latest === undefined) {
|
|
168
168
|
console.warn(`[doorbell] no comparable latest image version (${latestVersion ?? 'none'}) for runner ` +
|
|
169
|
-
`${
|
|
169
|
+
`${doorbellRequest.runnerId}; resuming on the running image ${runningVersion}`);
|
|
170
170
|
return false;
|
|
171
171
|
}
|
|
172
172
|
// Strictly below, not merely different: a running version ABOVE the latest
|
|
@@ -174,15 +174,15 @@ async function shouldRecreateForNewerImage(doorbell, shape, runningVersion, micr
|
|
|
174
174
|
// been deactivated, and recreating would not roll it forward. Resume.
|
|
175
175
|
return compareImageVersions(running, latest) < 0;
|
|
176
176
|
}
|
|
177
|
-
async function handleDoorbell({ rawBody, signatureHeader,
|
|
178
|
-
if (!(0, sdk_1.verifyEvidentSignature)(rawBody, signatureHeader,
|
|
177
|
+
async function handleDoorbell({ rawBody, signatureHeader, controllerSigningSecret, shapes, microvm, sleep, random, }) {
|
|
178
|
+
if (!(0, sdk_1.verifyEvidentSignature)(rawBody, signatureHeader, controllerSigningSecret)) {
|
|
179
179
|
return decide({
|
|
180
180
|
statusCode: 401,
|
|
181
181
|
action: 'rejected',
|
|
182
182
|
reason: REASON.invalidSignature,
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
|
-
const parsed = (0,
|
|
185
|
+
const parsed = (0, doorbell_request_1.parseDoorbellRequest)(rawBody);
|
|
186
186
|
if (!parsed.ok) {
|
|
187
187
|
return decide({
|
|
188
188
|
statusCode: 400,
|
|
@@ -190,10 +190,10 @@ async function handleDoorbell({ rawBody, signatureHeader, doorbellSecret, shapes
|
|
|
190
190
|
reason: parsed.reason,
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
|
-
const
|
|
193
|
+
const doorbellRequest = parsed.doorbell;
|
|
194
194
|
const timing = { sleep, random };
|
|
195
|
-
if (
|
|
196
|
-
// A pure read of deploy-time metadata — see doorbell.ts's header comment
|
|
195
|
+
if (doorbellRequest.type === doorbell_request_1.SHAPES_EVENT_TYPE) {
|
|
196
|
+
// A pure read of deploy-time metadata — see doorbell-request.ts's header comment
|
|
197
197
|
// (D2) for why replay is deliberately undefended here.
|
|
198
198
|
return decide({
|
|
199
199
|
statusCode: 200,
|
|
@@ -202,46 +202,46 @@ async function handleDoorbell({ rawBody, signatureHeader, doorbellSecret, shapes
|
|
|
202
202
|
shapes: shapes.advertise(),
|
|
203
203
|
});
|
|
204
204
|
}
|
|
205
|
-
// Resolve the requested shape immediately after
|
|
205
|
+
// Resolve the requested shape immediately after parseDoorbellRequest/the describe
|
|
206
206
|
// arm — BEFORE the suspend_requested branch and the whole state machine
|
|
207
207
|
// (D5). Resolving lazily inside runMicrovm would return 200 and silently
|
|
208
208
|
// ignore a removed shape on the RUNNING/PENDING no-op path and the
|
|
209
209
|
// successful-resume path, since runMicrovm is never reached on either. A
|
|
210
210
|
// suspend doorbell has no `shape` field, so nothing here runs for it.
|
|
211
|
-
if (
|
|
212
|
-
const resolvedShape = shapes.resolve(
|
|
211
|
+
if (doorbellRequest.type !== 'runner.suspend_requested') {
|
|
212
|
+
const resolvedShape = shapes.resolve(doorbellRequest.shape);
|
|
213
213
|
if (resolvedShape === undefined) {
|
|
214
214
|
// 400, not 404/409: a redelivery of the identical body cannot fix this.
|
|
215
215
|
// Never echo the requested shape name — the closed-constant rule
|
|
216
|
-
// (doorbell.ts's DOORBELL_REJECTION) applies to every reason here too,
|
|
216
|
+
// (doorbell-request.ts's DOORBELL_REJECTION) applies to every reason here too,
|
|
217
217
|
// and Evident persists response_body.
|
|
218
218
|
return decide({
|
|
219
219
|
statusCode: 400,
|
|
220
220
|
action: 'rejected',
|
|
221
221
|
reason: REASON.unknownShape,
|
|
222
|
-
runnerId:
|
|
223
|
-
microvmId:
|
|
222
|
+
runnerId: doorbellRequest.runnerId,
|
|
223
|
+
microvmId: doorbellRequest.microvmId,
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
|
-
return handleWakeDoorbell(
|
|
226
|
+
return handleWakeDoorbell(doorbellRequest, resolvedShape, microvm, timing);
|
|
227
227
|
}
|
|
228
|
-
return suspendMicrovm(
|
|
228
|
+
return suspendMicrovm(doorbellRequest, microvm, timing);
|
|
229
229
|
}
|
|
230
|
-
async function handleWakeDoorbell(
|
|
231
|
-
if (
|
|
232
|
-
return runMicrovm(
|
|
230
|
+
async function handleWakeDoorbell(doorbellRequest, shape, microvm, timing) {
|
|
231
|
+
if (doorbellRequest.microvmId === undefined) {
|
|
232
|
+
return runMicrovm(doorbellRequest, shape, microvm, timing, false, REASON.noMicrovmId);
|
|
233
233
|
}
|
|
234
234
|
let described;
|
|
235
235
|
let polled = false;
|
|
236
236
|
try {
|
|
237
|
-
described = await microvm.describe(
|
|
237
|
+
described = await microvm.describe(doorbellRequest.microvmId);
|
|
238
238
|
if (described.state === 'SUSPENDING') {
|
|
239
|
-
described = await pollWhileSuspending(
|
|
239
|
+
described = await pollWhileSuspending(doorbellRequest.microvmId, microvm, timing.sleep);
|
|
240
240
|
polled = true;
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
catch (error) {
|
|
244
|
-
console.error(`[doorbell] GetMicrovm failed for runner ${
|
|
244
|
+
console.error(`[doorbell] GetMicrovm failed for runner ${doorbellRequest.runnerId}: ` +
|
|
245
245
|
`${error instanceof Error ? error.message : String(error)}`);
|
|
246
246
|
// `polled` is omitted here, not passed as `false`: it is only assigned
|
|
247
247
|
// `true` after `pollWhileSuspending` returns, so a throw mid-poll would
|
|
@@ -250,8 +250,8 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
250
250
|
statusCode: 500,
|
|
251
251
|
action: 'none',
|
|
252
252
|
reason: REASON.getStateFailed,
|
|
253
|
-
runnerId:
|
|
254
|
-
microvmId:
|
|
253
|
+
runnerId: doorbellRequest.runnerId,
|
|
254
|
+
microvmId: doorbellRequest.microvmId,
|
|
255
255
|
});
|
|
256
256
|
}
|
|
257
257
|
const state = described.state;
|
|
@@ -264,8 +264,8 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
264
264
|
action: 'none',
|
|
265
265
|
reason: REASON.suspendingPollExhausted,
|
|
266
266
|
state,
|
|
267
|
-
runnerId:
|
|
268
|
-
microvmId:
|
|
267
|
+
runnerId: doorbellRequest.runnerId,
|
|
268
|
+
microvmId: doorbellRequest.microvmId,
|
|
269
269
|
polled,
|
|
270
270
|
});
|
|
271
271
|
}
|
|
@@ -279,8 +279,8 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
279
279
|
action: 'none',
|
|
280
280
|
reason: state === 'RUNNING' ? REASON.alreadyRunning : REASON.alreadyStarting,
|
|
281
281
|
state,
|
|
282
|
-
runnerId:
|
|
283
|
-
microvmId:
|
|
282
|
+
runnerId: doorbellRequest.runnerId,
|
|
283
|
+
microvmId: doorbellRequest.microvmId,
|
|
284
284
|
polled,
|
|
285
285
|
startedAt: described.startedAt,
|
|
286
286
|
imageVersion: described.imageVersion,
|
|
@@ -290,15 +290,15 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
290
290
|
// request (#1906) can still set the same wire flag. Recreate loses VM-local
|
|
291
291
|
// filesystem state; durable provider credentials survive in object storage,
|
|
292
292
|
// but a manual paste/upload not yet synced there may be lost (#2071).
|
|
293
|
-
if (
|
|
294
|
-
(await shouldRecreateForNewerImage(
|
|
295
|
-
return runMicrovm(
|
|
293
|
+
if (doorbellRequest.recreateOnOutdatedImage === true &&
|
|
294
|
+
(await shouldRecreateForNewerImage(doorbellRequest, shape, described.imageVersion, microvm))) {
|
|
295
|
+
return runMicrovm(doorbellRequest, shape, microvm, timing, polled, REASON.imageVersionOutdated, state);
|
|
296
296
|
}
|
|
297
|
-
return resumeMicrovm(
|
|
297
|
+
return resumeMicrovm(doorbellRequest, doorbellRequest.microvmId, shape, microvm, timing, polled, described.startedAt, described.imageVersion);
|
|
298
298
|
default:
|
|
299
299
|
// TERMINATED, TERMINATING, not_found — gone or going, so a fresh VM either
|
|
300
300
|
// way. This is what self-heals a stale `microvm_id`.
|
|
301
|
-
return runMicrovm(
|
|
301
|
+
return runMicrovm(doorbellRequest, shape, microvm, timing, polled, state === 'not_found' ? REASON.microvmNotFound : REASON.terminated, state);
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
/**
|
|
@@ -333,13 +333,13 @@ function isClientTokenParameterMismatch(error) {
|
|
|
333
333
|
const message = typeof e.message === 'string' ? e.message.toLowerCase() : '';
|
|
334
334
|
return message.includes('clienttoken') && message.includes('different request parameters');
|
|
335
335
|
}
|
|
336
|
-
async function runMicrovm(
|
|
336
|
+
async function runMicrovm(doorbellRequest, shape, microvm, timing, polled, reason, state) {
|
|
337
337
|
let started;
|
|
338
338
|
try {
|
|
339
339
|
started = await (0, throttle_retry_1.withThrottleRetry)(() => microvm.run({
|
|
340
340
|
imageIdentifier: shape.imageArn,
|
|
341
|
-
runHookPayload:
|
|
342
|
-
clientToken: clientTokenFor(
|
|
341
|
+
runHookPayload: doorbellRequest.runHookPayload,
|
|
342
|
+
clientToken: clientTokenFor(doorbellRequest),
|
|
343
343
|
}), timing);
|
|
344
344
|
}
|
|
345
345
|
catch (error) {
|
|
@@ -353,7 +353,7 @@ async function runMicrovm(doorbell, shape, microvm, timing, polled, reason, stat
|
|
|
353
353
|
// wakes.
|
|
354
354
|
const name = error instanceof Error ? error.name : 'unknown';
|
|
355
355
|
const message = error instanceof Error ? error.message : String(error);
|
|
356
|
-
console.warn(`[doorbell] RunMicrovm clientToken already used for runner ${
|
|
356
|
+
console.warn(`[doorbell] RunMicrovm clientToken already used for runner ${doorbellRequest.runnerId}: ` +
|
|
357
357
|
`${name}: ${message}`);
|
|
358
358
|
// No `microvmId` (see the comment above), and therefore deliberately no
|
|
359
359
|
// `startedAt` either: `decide`'s invariant is that `microvm_started_at`
|
|
@@ -365,18 +365,18 @@ async function runMicrovm(doorbell, shape, microvm, timing, polled, reason, stat
|
|
|
365
365
|
action: 'run',
|
|
366
366
|
reason: REASON.runAlreadyStartedForToken,
|
|
367
367
|
state,
|
|
368
|
-
runnerId:
|
|
368
|
+
runnerId: doorbellRequest.runnerId,
|
|
369
369
|
polled,
|
|
370
370
|
});
|
|
371
371
|
}
|
|
372
|
-
console.error(`[doorbell] RunMicrovm failed for runner ${
|
|
372
|
+
console.error(`[doorbell] RunMicrovm failed for runner ${doorbellRequest.runnerId}: ` +
|
|
373
373
|
`${error instanceof Error ? error.message : String(error)}`);
|
|
374
374
|
return decide({
|
|
375
375
|
statusCode: 500,
|
|
376
376
|
action: 'run',
|
|
377
377
|
reason: REASON.runFailed,
|
|
378
378
|
state,
|
|
379
|
-
runnerId:
|
|
379
|
+
runnerId: doorbellRequest.runnerId,
|
|
380
380
|
polled,
|
|
381
381
|
});
|
|
382
382
|
}
|
|
@@ -385,14 +385,14 @@ async function runMicrovm(doorbell, shape, microvm, timing, polled, reason, stat
|
|
|
385
385
|
action: 'run',
|
|
386
386
|
reason,
|
|
387
387
|
state,
|
|
388
|
-
runnerId:
|
|
388
|
+
runnerId: doorbellRequest.runnerId,
|
|
389
389
|
microvmId: started.microvmId,
|
|
390
390
|
polled,
|
|
391
391
|
startedAt: started.startedAt,
|
|
392
392
|
imageVersion: started.imageVersion,
|
|
393
393
|
});
|
|
394
394
|
}
|
|
395
|
-
async function resumeMicrovm(
|
|
395
|
+
async function resumeMicrovm(doorbellRequest, microvmId, shape, microvm, timing, polled,
|
|
396
396
|
// Both values come from the `describe()` result that led here because
|
|
397
397
|
// `ResumeMicrovm` returns no lifecycle metadata of its own.
|
|
398
398
|
startedAt, imageVersion) {
|
|
@@ -400,24 +400,24 @@ startedAt, imageVersion) {
|
|
|
400
400
|
await (0, throttle_retry_1.withThrottleRetry)(() => microvm.resume(microvmId), timing);
|
|
401
401
|
}
|
|
402
402
|
catch (error) {
|
|
403
|
-
console.error(`[doorbell] ResumeMicrovm failed for runner ${
|
|
403
|
+
console.error(`[doorbell] ResumeMicrovm failed for runner ${doorbellRequest.runnerId}: ` +
|
|
404
404
|
`${error instanceof Error ? error.message : String(error)}`);
|
|
405
|
-
return runMicrovm(
|
|
405
|
+
return runMicrovm(doorbellRequest, shape, microvm, timing, polled, REASON.resumeFailedFallingBackToRun, 'SUSPENDED');
|
|
406
406
|
}
|
|
407
407
|
return decide({
|
|
408
408
|
statusCode: 200,
|
|
409
409
|
action: 'resume',
|
|
410
410
|
reason: REASON.resumed,
|
|
411
411
|
state: 'SUSPENDED',
|
|
412
|
-
runnerId:
|
|
412
|
+
runnerId: doorbellRequest.runnerId,
|
|
413
413
|
microvmId,
|
|
414
414
|
polled,
|
|
415
415
|
startedAt,
|
|
416
416
|
imageVersion,
|
|
417
417
|
});
|
|
418
418
|
}
|
|
419
|
-
async function suspendMicrovm(
|
|
420
|
-
const microvmId =
|
|
419
|
+
async function suspendMicrovm(doorbellRequest, microvm, timing) {
|
|
420
|
+
const microvmId = doorbellRequest.microvmId;
|
|
421
421
|
if (microvmId === undefined) {
|
|
422
422
|
// Nothing to suspend, and a redelivery would not change that — 200 so the
|
|
423
423
|
// doorbell is not retried, but loudly enough to notice a mis-wired sender.
|
|
@@ -425,7 +425,7 @@ async function suspendMicrovm(doorbell, microvm, timing) {
|
|
|
425
425
|
statusCode: 200,
|
|
426
426
|
action: 'none',
|
|
427
427
|
reason: REASON.suspendWithoutMicrovmId,
|
|
428
|
-
runnerId:
|
|
428
|
+
runnerId: doorbellRequest.runnerId,
|
|
429
429
|
});
|
|
430
430
|
}
|
|
431
431
|
let state;
|
|
@@ -434,13 +434,13 @@ async function suspendMicrovm(doorbell, microvm, timing) {
|
|
|
434
434
|
({ state } = await microvm.describe(microvmId));
|
|
435
435
|
}
|
|
436
436
|
catch (error) {
|
|
437
|
-
console.error(`[doorbell] GetMicrovm failed for runner ${
|
|
437
|
+
console.error(`[doorbell] GetMicrovm failed for runner ${doorbellRequest.runnerId}: ` +
|
|
438
438
|
`${error instanceof Error ? error.message : String(error)}`);
|
|
439
439
|
return decide({
|
|
440
440
|
statusCode: 500,
|
|
441
441
|
action: 'none',
|
|
442
442
|
reason: REASON.getStateFailed,
|
|
443
|
-
runnerId:
|
|
443
|
+
runnerId: doorbellRequest.runnerId,
|
|
444
444
|
microvmId,
|
|
445
445
|
});
|
|
446
446
|
}
|
|
@@ -453,7 +453,7 @@ async function suspendMicrovm(doorbell, microvm, timing) {
|
|
|
453
453
|
action: 'none',
|
|
454
454
|
reason: REASON.suspendAlreadyGone,
|
|
455
455
|
state,
|
|
456
|
-
runnerId:
|
|
456
|
+
runnerId: doorbellRequest.runnerId,
|
|
457
457
|
microvmId,
|
|
458
458
|
});
|
|
459
459
|
}
|
|
@@ -461,14 +461,14 @@ async function suspendMicrovm(doorbell, microvm, timing) {
|
|
|
461
461
|
await (0, throttle_retry_1.withThrottleRetry)(() => microvm.suspend(microvmId), timing);
|
|
462
462
|
}
|
|
463
463
|
catch (error) {
|
|
464
|
-
console.error(`[doorbell] SuspendMicrovm failed for runner ${
|
|
464
|
+
console.error(`[doorbell] SuspendMicrovm failed for runner ${doorbellRequest.runnerId}: ` +
|
|
465
465
|
`${error instanceof Error ? error.message : String(error)}`);
|
|
466
466
|
return decide({
|
|
467
467
|
statusCode: 500,
|
|
468
468
|
action: 'suspend',
|
|
469
469
|
reason: REASON.suspendFailed,
|
|
470
470
|
state,
|
|
471
|
-
runnerId:
|
|
471
|
+
runnerId: doorbellRequest.runnerId,
|
|
472
472
|
microvmId,
|
|
473
473
|
});
|
|
474
474
|
}
|
|
@@ -477,7 +477,7 @@ async function suspendMicrovm(doorbell, microvm, timing) {
|
|
|
477
477
|
action: 'suspend',
|
|
478
478
|
reason: REASON.suspended,
|
|
479
479
|
state,
|
|
480
|
-
runnerId:
|
|
480
|
+
runnerId: doorbellRequest.runnerId,
|
|
481
481
|
microvmId,
|
|
482
482
|
});
|
|
483
483
|
}
|
|
@@ -1,42 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
readonly
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
* credential-free URL (typically `https://github.com/<owner>/<repo>.git`):
|
|
11
|
-
* this string ships inside the shared snapshot, so a token in it would be
|
|
12
|
-
* baked into every VM. The credential arrives per-VM instead.
|
|
13
|
-
*/
|
|
14
|
-
readonly originUrl: string;
|
|
15
|
-
/** Directory to write the build context to. Removed and recreated. */
|
|
16
|
-
readonly destination: string;
|
|
17
|
-
/**
|
|
18
|
-
* Overlay scripts the image build runs as root and as `runner`. The overlay
|
|
19
|
-
* ships inside the shared snapshot, so it may install software but must carry
|
|
20
|
-
* no credential. When omitted, deterministic no-op scripts are written.
|
|
21
|
-
*/
|
|
22
|
-
readonly overlayDir?: string;
|
|
23
|
-
/**
|
|
24
|
-
* The published template to copy from. Defaults to the one inside this
|
|
25
|
-
* package; named directly by the tests, which assert against a template they
|
|
26
|
-
* built themselves rather than whichever `dist/` happens to be present.
|
|
27
|
-
*/
|
|
28
|
-
readonly templateDir?: string;
|
|
1
|
+
/** The fixed directory that contains the workspace in a staged build context. */
|
|
2
|
+
export declare const MICROVM_WORKSPACE_DIRECTORY = "workspace";
|
|
3
|
+
export interface StageMicrovmBuildContextOptions {
|
|
4
|
+
/** The customer's directory containing their Dockerfile and COPY sources. */
|
|
5
|
+
readonly buildContextPath: string;
|
|
6
|
+
/** A full local checkout to bake into the image's `workspace/` directory. */
|
|
7
|
+
readonly workspaceRepositoryPath: string;
|
|
8
|
+
/** Credential-free origin URL carried by the baked checkout. */
|
|
9
|
+
readonly workspaceOriginUrl: string;
|
|
29
10
|
}
|
|
30
11
|
/**
|
|
31
|
-
*
|
|
32
|
-
* per-phase hook scripts, the deployment overlay and the repository — and
|
|
33
|
-
* returns its path. The result is what `EvidentMicrovmConstruct`'s `imageSource`
|
|
34
|
-
* takes.
|
|
12
|
+
* Stages customer-owned image inputs in a fresh temporary directory.
|
|
35
13
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* copy of those files (#1528). A caller may supply an overlay; the default is
|
|
39
|
-
* deterministic no-op scripts. `repositoryPath` is the only required caller
|
|
40
|
-
* input.
|
|
14
|
+
* Staging keeps the image asset hash a pure function of the workspace commit while
|
|
15
|
+
* leaving the customer in control of the Dockerfile and its build context.
|
|
41
16
|
*/
|
|
42
|
-
export declare function
|
|
17
|
+
export declare function stageMicrovmBuildContext(options: StageMicrovmBuildContextOptions): string;
|
|
@@ -33,143 +33,56 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.MICROVM_WORKSPACE_DIRECTORY = void 0;
|
|
37
|
+
exports.stageMicrovmBuildContext = stageMicrovmBuildContext;
|
|
37
38
|
const node_child_process_1 = require("node:child_process");
|
|
38
39
|
const node_fs_1 = require("node:fs");
|
|
40
|
+
const node_os_1 = require("node:os");
|
|
39
41
|
const path = __importStar(require("node:path"));
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
* Resolved from the PACKAGE ROOT, THREE directories above `__dirname` — either
|
|
44
|
-
* compiled (`dist/microvm/image`) or via ts-node (`src/microvm/image`) — the
|
|
45
|
-
* same way `microvm/construct.ts` resolves the controller Lambda asset, but one
|
|
46
|
-
* `..` deeper because this file sits a directory below it. It is
|
|
47
|
-
* written by `scripts/build.ts` at PACKAGE build time, not at stage time, so a
|
|
48
|
-
* consumer needs no esbuild and no checkout of this repo (#1528).
|
|
49
|
-
*/
|
|
50
|
-
const TEMPLATE_DIR = path.resolve(__dirname, '..', '..', '..', 'dist', 'microvm-image-context');
|
|
42
|
+
/** The fixed directory that contains the workspace in a staged build context. */
|
|
43
|
+
exports.MICROVM_WORKSPACE_DIRECTORY = 'workspace';
|
|
51
44
|
function git(args, cwd) {
|
|
52
45
|
return (0, node_child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8' }).trim();
|
|
53
46
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
* error (e.g. "not a git repository") when `repositoryPath` isn't usable at
|
|
57
|
-
* all, so that failure is rethrown naming the path.
|
|
58
|
-
*/
|
|
59
|
-
function isShallowRepository(repositoryPath) {
|
|
47
|
+
/** Names the path when git cannot determine whether the repository is shallow. */
|
|
48
|
+
function isShallowRepository(sourcePath) {
|
|
60
49
|
try {
|
|
61
|
-
return git(['rev-parse', '--is-shallow-repository'],
|
|
50
|
+
return git(['rev-parse', '--is-shallow-repository'], sourcePath) === 'true';
|
|
62
51
|
}
|
|
63
52
|
catch (err) {
|
|
64
|
-
throw new Error(`${
|
|
53
|
+
throw new Error(`${sourcePath} is not a usable git repository: ` +
|
|
65
54
|
`${err instanceof Error ? err.message : String(err)}`);
|
|
66
55
|
}
|
|
67
56
|
}
|
|
68
57
|
/**
|
|
69
|
-
* Clones
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* `file://` rather than a plain path: a path triggers git's local-clone
|
|
73
|
-
* optimisation, which copies the whole object store — every branch and worktree
|
|
74
|
-
* ref on the staging machine (50 MB here). The file transport negotiates like a
|
|
75
|
-
* network fetch and packs only what the checked-out branch reaches (9 MB), with
|
|
76
|
-
* one branch and one remote-tracking ref. Ignored files, `node_modules` first
|
|
77
|
-
* among them, are not part of a clone at all.
|
|
58
|
+
* Clones a single branch with full history while normalising bytes that affect the image asset hash.
|
|
59
|
+
* Its remote refspec covers only that branch, so other branches need an explicit refspec.
|
|
78
60
|
*/
|
|
79
|
-
function stageRepository(
|
|
80
|
-
if (isShallowRepository(
|
|
81
|
-
throw new Error(`refusing to stage a shallow checkout (${
|
|
61
|
+
function stageRepository(sourcePath, stagedWorkspacePath, workspaceOriginUrl) {
|
|
62
|
+
if (isShallowRepository(sourcePath)) {
|
|
63
|
+
throw new Error(`refusing to stage a shallow checkout (${sourcePath}): the agent branches, ` +
|
|
82
64
|
'commits and opens PRs, so the image needs real history. Run `git fetch --unshallow`.');
|
|
83
65
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
git(['remote', 'set-url', 'origin', originUrl], destination);
|
|
91
|
-
// The asset hash of the resulting image must be a pure function of the commit,
|
|
92
|
-
// or every deploy publishes a new `AWS::Lambda::MicrovmImage` version and wedges
|
|
93
|
-
// the stack at its per-image version ceiling. Two things break that purity:
|
|
94
|
-
// - `pack-objects`' delta search is multithreaded (`pack.threads` defaults to
|
|
95
|
-
// the CPU count) and its output varies run to run; `-f`/`-F`
|
|
96
|
-
// (`--no-reuse-delta`/`--no-reuse-object`) additionally stop it from reusing
|
|
97
|
-
// whatever packing the source checkout happened to have. All three together
|
|
98
|
-
// make the pack a pure function of the object set, independent of the source.
|
|
99
|
-
// - `.git/index` carries stat data (inode/mtime/size) and `.git/logs/**` carries
|
|
100
|
-
// wall-clock reflog timestamps; neither is byte-stable for a fixed commit.
|
|
101
|
-
git(['-c', 'pack.threads=1', 'repack', '-adfqF'], destination);
|
|
102
|
-
(0, node_fs_1.rmSync)(path.join(destination, '.git', 'index'), { force: true });
|
|
103
|
-
(0, node_fs_1.rmSync)(path.join(destination, '.git', 'logs'), { recursive: true, force: true });
|
|
104
|
-
console.log(`[stage] repo ${git(['rev-parse', 'HEAD'], destination)}`);
|
|
105
|
-
}
|
|
106
|
-
function stageScriptDirectory(source, destination) {
|
|
107
|
-
(0, node_fs_1.mkdirSync)(destination);
|
|
108
|
-
for (const entry of (0, node_fs_1.readdirSync)(source, { withFileTypes: true })) {
|
|
109
|
-
const sourcePath = path.join(source, entry.name);
|
|
110
|
-
const staged = path.join(destination, entry.name);
|
|
111
|
-
if (entry.isDirectory()) {
|
|
112
|
-
stageScriptDirectory(sourcePath, staged);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
(0, node_fs_1.copyFileSync)(sourcePath, staged);
|
|
116
|
-
// Set modes here rather than inheriting them from the template, so they stay
|
|
117
|
-
// correct after an npm pack, zip, CI-cache or hand-copy round trip. The image
|
|
118
|
-
// executes extensionless scripts and sources `.sh` files.
|
|
119
|
-
(0, node_fs_1.chmodSync)(staged, entry.name.endsWith('.sh') ? 0o644 : 0o755);
|
|
120
|
-
}
|
|
66
|
+
git(['clone', '--quiet', '--single-branch', `file://${sourcePath}`, stagedWorkspacePath], path.dirname(stagedWorkspacePath));
|
|
67
|
+
git(['remote', 'set-url', 'origin', workspaceOriginUrl], stagedWorkspacePath);
|
|
68
|
+
git(['-c', 'pack.threads=1', 'repack', '-adfqF'], stagedWorkspacePath);
|
|
69
|
+
(0, node_fs_1.rmSync)(path.join(stagedWorkspacePath, '.git', 'index'), { force: true });
|
|
70
|
+
(0, node_fs_1.rmSync)(path.join(stagedWorkspacePath, '.git', 'logs'), { recursive: true, force: true });
|
|
71
|
+
console.log(`[stage] repo ${git(['rev-parse', 'HEAD'], stagedWorkspacePath)}`);
|
|
121
72
|
}
|
|
122
73
|
/**
|
|
123
|
-
*
|
|
124
|
-
* per-phase hook scripts, the deployment overlay and the repository — and
|
|
125
|
-
* returns its path. The result is what `EvidentMicrovmConstruct`'s `imageSource`
|
|
126
|
-
* takes.
|
|
74
|
+
* Stages customer-owned image inputs in a fresh temporary directory.
|
|
127
75
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* copy of those files (#1528). A caller may supply an overlay; the default is
|
|
131
|
-
* deterministic no-op scripts. `repositoryPath` is the only required caller
|
|
132
|
-
* input.
|
|
76
|
+
* Staging keeps the image asset hash a pure function of the workspace commit while
|
|
77
|
+
* leaving the customer in control of the Dockerfile and its build context.
|
|
133
78
|
*/
|
|
134
|
-
function
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const repositoryPath = path.resolve(options.repositoryPath);
|
|
139
|
-
const destination = path.resolve(options.destination);
|
|
140
|
-
if (!(0, node_fs_1.existsSync)(templateDir)) {
|
|
141
|
-
throw new Error(`the MicroVM image context template is missing from ${templateDir}. It is written by ` +
|
|
142
|
-
"this package's build (`pnpm --filter @evident-ai/runner-cdk build`) and ships in the " +
|
|
143
|
-
'published tarball, so an installed copy should always have it.');
|
|
144
|
-
}
|
|
145
|
-
(0, node_fs_1.rmSync)(destination, { recursive: true, force: true });
|
|
146
|
-
(0, node_fs_1.mkdirSync)(destination, { recursive: true });
|
|
147
|
-
(0, node_fs_1.copyFileSync)(path.join(templateDir, 'Dockerfile'), path.join(destination, 'Dockerfile'));
|
|
148
|
-
(0, node_fs_1.copyFileSync)(path.join(templateDir, 'hook-server.js'), path.join(destination, 'hook-server.js'));
|
|
149
|
-
const hooksSource = path.join(templateDir, 'hooks');
|
|
150
|
-
const hooksStage = path.join(destination, 'hooks');
|
|
151
|
-
stageScriptDirectory(hooksSource, hooksStage);
|
|
152
|
-
const overlayStage = path.join(destination, 'overlay');
|
|
153
|
-
if (overlayDir === undefined) {
|
|
154
|
-
(0, node_fs_1.mkdirSync)(overlayStage);
|
|
155
|
-
for (const name of ['setup-root', 'setup-workspace']) {
|
|
156
|
-
const staged = path.join(overlayStage, name);
|
|
157
|
-
(0, node_fs_1.writeFileSync)(staged, '#!/usr/bin/env bash\n# No overlay supplied.\n', { mode: 0o755 });
|
|
158
|
-
(0, node_fs_1.chmodSync)(staged, 0o755);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
const resolvedOverlayDir = path.resolve(overlayDir);
|
|
163
|
-
if (!(0, node_fs_1.existsSync)(resolvedOverlayDir)) {
|
|
164
|
-
throw new Error(`overlay directory does not exist: ${resolvedOverlayDir}`);
|
|
165
|
-
}
|
|
166
|
-
for (const required of ['setup-root', 'setup-workspace']) {
|
|
167
|
-
if (!(0, node_fs_1.existsSync)(path.join(resolvedOverlayDir, required))) {
|
|
168
|
-
throw new Error(`overlay directory ${resolvedOverlayDir} is missing required script ${required}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
stageScriptDirectory(resolvedOverlayDir, overlayStage);
|
|
79
|
+
function stageMicrovmBuildContext(options) {
|
|
80
|
+
const buildContextPath = path.resolve(options.buildContextPath);
|
|
81
|
+
if (!(0, node_fs_1.existsSync)(buildContextPath) || !(0, node_fs_1.existsSync)(path.join(buildContextPath, 'Dockerfile'))) {
|
|
82
|
+
throw new Error(`MicroVM build context ${buildContextPath} does not exist or has no Dockerfile`);
|
|
172
83
|
}
|
|
173
|
-
|
|
174
|
-
|
|
84
|
+
const stagedPath = (0, node_fs_1.mkdtempSync)(path.join((0, node_os_1.tmpdir)(), 'evident-microvm-'));
|
|
85
|
+
(0, node_fs_1.cpSync)(buildContextPath, stagedPath, { recursive: true });
|
|
86
|
+
stageRepository(path.resolve(options.workspaceRepositoryPath), path.join(stagedPath, exports.MICROVM_WORKSPACE_DIRECTORY), options.workspaceOriginUrl);
|
|
87
|
+
return stagedPath;
|
|
175
88
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Construct } from 'constructs';
|
|
2
|
+
import type { MicrovmImageBuildInputs } from '@evident-ai/lambda-microvm-cdk';
|
|
3
|
+
export type ImageVersionPrunerProps = {
|
|
4
|
+
readonly imageArns: readonly string[];
|
|
5
|
+
/**
|
|
6
|
+
* Build inputs are a CloudFormation-visible invocation trigger. The build role
|
|
7
|
+
* ARN is excluded because it belongs to the image subtree and would create a cycle.
|
|
8
|
+
*/
|
|
9
|
+
readonly buildTriggers: Record<string, Omit<MicrovmImageBuildInputs, 'buildRoleArn'>>;
|
|
10
|
+
};
|
|
11
|
+
export declare class ImageVersionPruner extends Construct {
|
|
12
|
+
constructor(scope: Construct, id: string, props: ImageVersionPrunerProps);
|
|
13
|
+
}
|