@evident-ai/runner-cdk 3.4.1-dev.51df4de → 3.4.1-dev.ab61560
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 +1 -0
- package/dist/controller-lambda/handler.js +13 -7
- package/dist/image-version-reporter-lambda/handler.js +129 -0
- package/dist/microvm/construct.d.ts +18 -0
- package/dist/microvm/construct.js +26 -0
- package/dist/microvm/controller/handle-doorbell.js +9 -6
- package/dist/microvm/controller/microvm-client.d.ts +6 -0
- package/dist/microvm/image-version-reporter/construct.d.ts +35 -0
- package/dist/microvm/image-version-reporter/construct.js +91 -0
- package/dist/microvm/image-version-reporter/handler.d.ts +26 -0
- package/dist/microvm/image-version-reporter/handler.js +104 -0
- package/dist/microvm-image-context/hooks/common.sh +88 -826
- package/dist/microvm-image-context/hooks/resume +14 -12
- package/dist/microvm-image-context/hooks/run +20 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,6 +165,7 @@ and the agent installs on first use.
|
|
|
165
165
|
| `runnerSecret` | No GitHub or MCP credentials are exported at `/run`. When supplied, `/run` reads the JSON secret and exports every non-empty value whose key is a valid environment-variable name. |
|
|
166
166
|
| `runnerOpencodeConfigPath` | OpenCode uses the baked project configuration. Relative paths resolve from the workspace; absolute paths resolve in the image. |
|
|
167
167
|
| `gitUserName` / `gitUserEmail` | The hook uses the Evident bot defaults for git identity. |
|
|
168
|
+
| `evidentApiUrl` / `evidentRunnerId` | No deploy-time report of the published image version — a byte-identical template to not having these props at all. Both non-secret (a URL, a public pool UUID) and both required together: set them if you have an Evident runner id configured for this provisioner, so each deploy that publishes a new image version tells Evident about it (a CloudFormation custom resource, HMAC-signed with `doorbellSecret`). A failure to report never fails your deploy — it warns and Evident reads that provisioner as *unknown*, never as up to date or stale. |
|
|
168
169
|
|
|
169
170
|
## Status / limitations
|
|
170
171
|
|
|
@@ -49949,6 +49949,7 @@ function decide({
|
|
|
49949
49949
|
runnerId,
|
|
49950
49950
|
microvmId,
|
|
49951
49951
|
startedAt,
|
|
49952
|
+
imageVersion,
|
|
49952
49953
|
shapes,
|
|
49953
49954
|
polled
|
|
49954
49955
|
}) {
|
|
@@ -49966,6 +49967,7 @@ function decide({
|
|
|
49966
49967
|
reason,
|
|
49967
49968
|
...microvmId === void 0 ? {} : { microvm_id: microvmId },
|
|
49968
49969
|
...startedAt === void 0 ? {} : { microvm_started_at: startedAt.toISOString() },
|
|
49970
|
+
...imageVersion === void 0 ? {} : { image_version: imageVersion },
|
|
49969
49971
|
...shapes === void 0 ? {} : { shapes },
|
|
49970
49972
|
...polled === true ? { polled: true } : {}
|
|
49971
49973
|
}),
|
|
@@ -50122,7 +50124,8 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
50122
50124
|
runnerId: doorbell.runnerId,
|
|
50123
50125
|
microvmId: doorbell.microvmId,
|
|
50124
50126
|
polled,
|
|
50125
|
-
startedAt: described.startedAt
|
|
50127
|
+
startedAt: described.startedAt,
|
|
50128
|
+
imageVersion: described.imageVersion
|
|
50126
50129
|
});
|
|
50127
50130
|
case "SUSPENDED":
|
|
50128
50131
|
if (await shouldRecreateForNewerImage(doorbell, shape, described.imageVersion, microvm)) {
|
|
@@ -50143,7 +50146,8 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
50143
50146
|
microvm,
|
|
50144
50147
|
timing,
|
|
50145
50148
|
polled,
|
|
50146
|
-
described.startedAt
|
|
50149
|
+
described.startedAt,
|
|
50150
|
+
described.imageVersion
|
|
50147
50151
|
);
|
|
50148
50152
|
default:
|
|
50149
50153
|
return runMicrovm(
|
|
@@ -50220,10 +50224,11 @@ async function runMicrovm(doorbell, shape, microvm, timing, polled, reason, stat
|
|
|
50220
50224
|
runnerId: doorbell.runnerId,
|
|
50221
50225
|
microvmId: started.microvmId,
|
|
50222
50226
|
polled,
|
|
50223
|
-
startedAt: started.startedAt
|
|
50227
|
+
startedAt: started.startedAt,
|
|
50228
|
+
imageVersion: started.imageVersion
|
|
50224
50229
|
});
|
|
50225
50230
|
}
|
|
50226
|
-
async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled, startedAt) {
|
|
50231
|
+
async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled, startedAt, imageVersion) {
|
|
50227
50232
|
try {
|
|
50228
50233
|
await withThrottleRetry(() => microvm.resume(microvmId), timing);
|
|
50229
50234
|
} catch (error3) {
|
|
@@ -50248,7 +50253,8 @@ async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled
|
|
|
50248
50253
|
runnerId: doorbell.runnerId,
|
|
50249
50254
|
microvmId,
|
|
50250
50255
|
polled,
|
|
50251
|
-
startedAt
|
|
50256
|
+
startedAt,
|
|
50257
|
+
imageVersion
|
|
50252
50258
|
});
|
|
50253
50259
|
}
|
|
50254
50260
|
async function suspendMicrovm(doorbell, microvm, timing) {
|
|
@@ -50443,7 +50449,7 @@ function createRuntimeMicrovm(executionRoleArn) {
|
|
|
50443
50449
|
return latestActiveImageVersion;
|
|
50444
50450
|
},
|
|
50445
50451
|
async run({ imageIdentifier, runHookPayload, clientToken }) {
|
|
50446
|
-
const { microvmId, startedAt } = await microvms.send(
|
|
50452
|
+
const { microvmId, startedAt, imageVersion } = await microvms.send(
|
|
50447
50453
|
new import_client_lambda_microvms.RunMicrovmCommand({
|
|
50448
50454
|
imageIdentifier,
|
|
50449
50455
|
executionRoleArn,
|
|
@@ -50476,7 +50482,7 @@ function createRuntimeMicrovm(executionRoleArn) {
|
|
|
50476
50482
|
if (!microvmId) {
|
|
50477
50483
|
throw new Error("RunMicrovm returned no microvmId");
|
|
50478
50484
|
}
|
|
50479
|
-
return { microvmId, startedAt };
|
|
50485
|
+
return { microvmId, startedAt, imageVersion };
|
|
50480
50486
|
},
|
|
50481
50487
|
async resume(microvmId) {
|
|
50482
50488
|
await microvms.send(new import_client_lambda_microvms.ResumeMicrovmCommand({ microvmIdentifier: microvmId }));
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/microvm/image-version-reporter/handler.ts
|
|
21
|
+
var handler_exports = {};
|
|
22
|
+
__export(handler_exports, {
|
|
23
|
+
handleImageVersionReport: () => handleImageVersionReport,
|
|
24
|
+
handler: () => handler
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(handler_exports);
|
|
27
|
+
var import_node_crypto = require("node:crypto");
|
|
28
|
+
var import_client_secrets_manager = require("@aws-sdk/client-secrets-manager");
|
|
29
|
+
var PHYSICAL_RESOURCE_ID = "evident-image-version-report";
|
|
30
|
+
var REPORT_TIMEOUT_MS = 5e3;
|
|
31
|
+
var secrets = new import_client_secrets_manager.SecretsManagerClient({});
|
|
32
|
+
async function fetchDoorbellSecret(secretArn, secretKey) {
|
|
33
|
+
const { SecretString } = await secrets.send(new import_client_secrets_manager.GetSecretValueCommand({ SecretId: secretArn }));
|
|
34
|
+
if (!SecretString) {
|
|
35
|
+
throw new Error(`doorbell secret ${secretArn} has no SecretString`);
|
|
36
|
+
}
|
|
37
|
+
const parsed = JSON.parse(SecretString);
|
|
38
|
+
const value = parsed !== null && typeof parsed === "object" ? parsed[secretKey] : void 0;
|
|
39
|
+
if (typeof value !== "string" || value === "") {
|
|
40
|
+
throw new Error(`doorbell secret ${secretArn} is missing a non-empty '${secretKey}' field`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function requireEnv(name) {
|
|
45
|
+
const value = process.env[name];
|
|
46
|
+
if (!value) {
|
|
47
|
+
throw new Error(`missing required env var ${name}`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function readImageVersions(value) {
|
|
52
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
53
|
+
throw new Error("imageVersions must be an object");
|
|
54
|
+
}
|
|
55
|
+
return Object.entries(value).map(([shape, imageVersion]) => {
|
|
56
|
+
if (typeof imageVersion !== "string") {
|
|
57
|
+
throw new Error(`imageVersions.${shape} must be a string`);
|
|
58
|
+
}
|
|
59
|
+
return { shape, image_version: imageVersion };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function response() {
|
|
63
|
+
return { PhysicalResourceId: PHYSICAL_RESOURCE_ID };
|
|
64
|
+
}
|
|
65
|
+
var runtimeDependencies = {
|
|
66
|
+
getSecret: fetchDoorbellSecret,
|
|
67
|
+
fetch: globalThis.fetch
|
|
68
|
+
};
|
|
69
|
+
async function handleImageVersionReport(event, getConfig, dependencies = runtimeDependencies) {
|
|
70
|
+
if (event.RequestType === "Delete") {
|
|
71
|
+
return response();
|
|
72
|
+
}
|
|
73
|
+
let runnerId = "unknown";
|
|
74
|
+
let versions = [];
|
|
75
|
+
let responseStatus;
|
|
76
|
+
try {
|
|
77
|
+
versions = readImageVersions(event.ResourceProperties?.imageVersions);
|
|
78
|
+
const config = getConfig();
|
|
79
|
+
runnerId = config.evidentRunnerId;
|
|
80
|
+
const secret = await dependencies.getSecret(config.doorbellSecretArn, config.doorbellSecretKey);
|
|
81
|
+
const body = JSON.stringify({
|
|
82
|
+
type: "runner.microvm_image_versions_reported",
|
|
83
|
+
versions
|
|
84
|
+
});
|
|
85
|
+
const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(body).digest("hex");
|
|
86
|
+
const url = `${config.evidentApiUrl.replace(/\/+$/, "")}/v1/runners/${encodeURIComponent(
|
|
87
|
+
runnerId
|
|
88
|
+
)}/microvm-image-versions`;
|
|
89
|
+
const result = await dependencies.fetch(url, {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
"content-type": "application/json",
|
|
93
|
+
"x-evident-signature": signature
|
|
94
|
+
},
|
|
95
|
+
body,
|
|
96
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
|
|
97
|
+
});
|
|
98
|
+
responseStatus = result.status;
|
|
99
|
+
console.log("runner.microvm_image_versions_reported", {
|
|
100
|
+
runnerId,
|
|
101
|
+
versions,
|
|
102
|
+
status: result.status
|
|
103
|
+
});
|
|
104
|
+
if (!result.ok) {
|
|
105
|
+
throw new Error(`Evident returned HTTP ${result.status}`);
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
console.warn("MicroVM image version report failed", {
|
|
110
|
+
operation: event.RequestType,
|
|
111
|
+
runnerId,
|
|
112
|
+
versions,
|
|
113
|
+
...responseStatus === void 0 ? {} : { status: responseStatus },
|
|
114
|
+
error: message
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return response();
|
|
118
|
+
}
|
|
119
|
+
var handler = (event) => handleImageVersionReport(event, () => ({
|
|
120
|
+
doorbellSecretArn: requireEnv("DOORBELL_SECRET_ARN"),
|
|
121
|
+
doorbellSecretKey: requireEnv("DOORBELL_SECRET_KEY"),
|
|
122
|
+
evidentApiUrl: requireEnv("EVIDENT_API_URL"),
|
|
123
|
+
evidentRunnerId: requireEnv("EVIDENT_RUNNER_ID")
|
|
124
|
+
}));
|
|
125
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
126
|
+
0 && (module.exports = {
|
|
127
|
+
handleImageVersionReport,
|
|
128
|
+
handler
|
|
129
|
+
});
|
|
@@ -69,6 +69,24 @@ export interface EvidentMicrovmConstructProps {
|
|
|
69
69
|
* running a hook).
|
|
70
70
|
*/
|
|
71
71
|
readonly hookTimeoutSeconds: number;
|
|
72
|
+
/**
|
|
73
|
+
* Base URL of Evident's own API. Together with `evidentRunnerId`, opts this
|
|
74
|
+
* deploy into reporting each shape's published image version to Evident
|
|
75
|
+
* (#1903) — a CDK custom resource, triggered only when a deploy actually
|
|
76
|
+
* changes a version. Omitting BOTH leaves the template byte-for-byte
|
|
77
|
+
* identical to not having this prop at all: an adopter with no Evident
|
|
78
|
+
* runner id configured has nothing to report against. Supplying only one
|
|
79
|
+
* throws at synth — see `evidentRunnerId`.
|
|
80
|
+
*/
|
|
81
|
+
readonly evidentApiUrl?: string;
|
|
82
|
+
/**
|
|
83
|
+
* The pool runner id (public, non-secret UUID) to report against — see
|
|
84
|
+
* `evidentApiUrl`. Mirrors `EvidentScaleToZeroConstructProps.evidentAgentId`'s
|
|
85
|
+
* "public (non-secret) Evident agent UUID" precedent. Must be supplied
|
|
86
|
+
* together with `evidentApiUrl`, or neither at all — supplying exactly one
|
|
87
|
+
* throws at construction rather than silently disabling the reporter.
|
|
88
|
+
*/
|
|
89
|
+
readonly evidentRunnerId?: string;
|
|
72
90
|
}
|
|
73
91
|
/**
|
|
74
92
|
* The customer-account half of the per-session runner (#558): the MicroVM
|
|
@@ -41,6 +41,7 @@ const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
|
|
|
41
41
|
const s3 = __importStar(require("aws-cdk-lib/aws-s3"));
|
|
42
42
|
const lambda_microvm_cdk_1 = require("@evident-ai/lambda-microvm-cdk");
|
|
43
43
|
const shapes_1 = require("./shapes");
|
|
44
|
+
const construct_1 = require("./image-version-reporter/construct");
|
|
44
45
|
// Image hooks build the snapshot, so they get minutes where the runtime hooks
|
|
45
46
|
// get AWS's 60 s.
|
|
46
47
|
const IMAGE_HOOK_TIMEOUT_SECONDS = 600;
|
|
@@ -65,6 +66,15 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
65
66
|
// Synth-time failure, before any construct exists.
|
|
66
67
|
const shapes = (0, shapes_1.validateShapes)(props.shapes ?? shapes_1.MICROVM_SHAPES);
|
|
67
68
|
const [defaultShape, ...additionalShapes] = shapes;
|
|
69
|
+
// D8's "both required together" is a synth-time contract, not just a
|
|
70
|
+
// README sentence: a caller supplying exactly one silently gets NO
|
|
71
|
+
// reporter and no warning (the `&&` check below just skips it), which
|
|
72
|
+
// reads as "configured" when it is not. Fail loudly instead.
|
|
73
|
+
if (Boolean(props.evidentApiUrl) !== Boolean(props.evidentRunnerId)) {
|
|
74
|
+
throw new Error('EvidentMicrovmConstruct: evidentApiUrl and evidentRunnerId must be supplied ' +
|
|
75
|
+
'together or not at all — supplying only one silently disables the image ' +
|
|
76
|
+
'version reporter rather than reporting a misconfiguration.');
|
|
77
|
+
}
|
|
68
78
|
// The model credentials a runner restores at /run, and the litestream
|
|
69
79
|
// session replica (it is `LITESTREAM_BUCKET` below), share one per-runner
|
|
70
80
|
// prefix chosen at /run — synth can't name it, and lifecycle filters take
|
|
@@ -249,6 +259,22 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
249
259
|
// HMAC verification of the doorbell body IS the auth, as for the ECS waker.
|
|
250
260
|
authType: lambda.FunctionUrlAuthType.NONE,
|
|
251
261
|
});
|
|
262
|
+
// Opt-in only (D8): both values present is what creates the reporter, so an
|
|
263
|
+
// adopter who supplies neither gets a template with no additional Lambda and
|
|
264
|
+
// no custom resource — this branch must not run for them.
|
|
265
|
+
if (props.evidentApiUrl && props.evidentRunnerId) {
|
|
266
|
+
new construct_1.ImageVersionReporter(this, 'ImageVersionReporter', {
|
|
267
|
+
doorbellSecret: props.doorbellSecret,
|
|
268
|
+
evidentApiUrl: props.evidentApiUrl,
|
|
269
|
+
evidentRunnerId: props.evidentRunnerId,
|
|
270
|
+
imageVersions: Object.fromEntries(shapes.map((shape) => [shape.name, images.get(shape.name).latestActiveImageVersion])),
|
|
271
|
+
// `buildInputs` already covers every property that can cause AWS
|
|
272
|
+
// to publish a new version for THIS shape's image (source, base
|
|
273
|
+
// image, memory, hooks, environment, description, build role —
|
|
274
|
+
// see its docstring); no additional encoding needed here.
|
|
275
|
+
buildTriggers: Object.fromEntries(shapes.map((shape) => [shape.name, images.get(shape.name).buildInputs])),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
252
278
|
this.functionUrl = functionUrl.url;
|
|
253
279
|
this.shapeCatalogueJson = shapeCatalogueJson;
|
|
254
280
|
this.durableStateBucket = durableState;
|
|
@@ -37,7 +37,7 @@ const LOG_ACTION = {
|
|
|
37
37
|
rejected: 'reject',
|
|
38
38
|
describe: 'describe',
|
|
39
39
|
};
|
|
40
|
-
function decide({ statusCode, action, reason, state, runnerId, microvmId, startedAt, shapes, polled, }) {
|
|
40
|
+
function decide({ statusCode, action, reason, state, runnerId, microvmId, startedAt, imageVersion, shapes, polled, }) {
|
|
41
41
|
const line = `[doorbell] state=${state ?? 'none'} action=${LOG_ACTION[action]} ` +
|
|
42
42
|
`microvm_id=${microvmId ?? '-'} runner_id=${runnerId ?? '-'} reason=${reason}` +
|
|
43
43
|
(polled === undefined ? '' : ` polled=${polled}`);
|
|
@@ -56,6 +56,7 @@ function decide({ statusCode, action, reason, state, runnerId, microvmId, starte
|
|
|
56
56
|
reason,
|
|
57
57
|
...(microvmId === undefined ? {} : { microvm_id: microvmId }),
|
|
58
58
|
...(startedAt === undefined ? {} : { microvm_started_at: startedAt.toISOString() }),
|
|
59
|
+
...(imageVersion === undefined ? {} : { image_version: imageVersion }),
|
|
59
60
|
...(shapes === undefined ? {} : { shapes }),
|
|
60
61
|
...(polled === true ? { polled: true } : {}),
|
|
61
62
|
}),
|
|
@@ -283,6 +284,7 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
283
284
|
microvmId: doorbell.microvmId,
|
|
284
285
|
polled,
|
|
285
286
|
startedAt: described.startedAt,
|
|
287
|
+
imageVersion: described.imageVersion,
|
|
286
288
|
});
|
|
287
289
|
case 'SUSPENDED':
|
|
288
290
|
// A newer image has been published since this VM booted: resuming would
|
|
@@ -292,7 +294,7 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
292
294
|
if (await shouldRecreateForNewerImage(doorbell, shape, described.imageVersion, microvm)) {
|
|
293
295
|
return runMicrovm(doorbell, shape, microvm, timing, polled, REASON.imageVersionOutdated, state);
|
|
294
296
|
}
|
|
295
|
-
return resumeMicrovm(doorbell, doorbell.microvmId, shape, microvm, timing, polled, described.startedAt);
|
|
297
|
+
return resumeMicrovm(doorbell, doorbell.microvmId, shape, microvm, timing, polled, described.startedAt, described.imageVersion);
|
|
296
298
|
default:
|
|
297
299
|
// TERMINATED, TERMINATING, not_found — gone or going, so a fresh VM either
|
|
298
300
|
// way. This is what self-heals a stale `microvm_id`.
|
|
@@ -387,13 +389,13 @@ async function runMicrovm(doorbell, shape, microvm, timing, polled, reason, stat
|
|
|
387
389
|
microvmId: started.microvmId,
|
|
388
390
|
polled,
|
|
389
391
|
startedAt: started.startedAt,
|
|
392
|
+
imageVersion: started.imageVersion,
|
|
390
393
|
});
|
|
391
394
|
}
|
|
392
395
|
async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled,
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
startedAt) {
|
|
396
|
+
// Both values come from the `describe()` result that led here because
|
|
397
|
+
// `ResumeMicrovm` returns no lifecycle metadata of its own.
|
|
398
|
+
startedAt, imageVersion) {
|
|
397
399
|
try {
|
|
398
400
|
await (0, throttle_retry_1.withThrottleRetry)(() => microvm.resume(microvmId), timing);
|
|
399
401
|
}
|
|
@@ -411,6 +413,7 @@ startedAt) {
|
|
|
411
413
|
microvmId,
|
|
412
414
|
polled,
|
|
413
415
|
startedAt,
|
|
416
|
+
imageVersion,
|
|
414
417
|
});
|
|
415
418
|
}
|
|
416
419
|
async function suspendMicrovm(doorbell, microvm, timing) {
|
|
@@ -69,6 +69,12 @@ export interface MicrovmClient {
|
|
|
69
69
|
* none. Never synthesize one (e.g. `new Date()`) here or in any caller.
|
|
70
70
|
*/
|
|
71
71
|
startedAt?: Date;
|
|
72
|
+
/**
|
|
73
|
+
* The image version this VM booted on, straight from `RunMicrovm`'s own
|
|
74
|
+
* `imageVersion`. Absent means AWS omitted it and must be read as UNKNOWN;
|
|
75
|
+
* see `MicrovmDescription.imageVersion` above.
|
|
76
|
+
*/
|
|
77
|
+
imageVersion?: string;
|
|
72
78
|
}>;
|
|
73
79
|
resume(microvmId: string): Promise<void>;
|
|
74
80
|
suspend(microvmId: string): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Construct } from 'constructs';
|
|
2
|
+
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
|
|
3
|
+
import type { MicrovmImageBuildInputs } from '@evident-ai/lambda-microvm-cdk';
|
|
4
|
+
export type ImageVersionReporterProps = {
|
|
5
|
+
/** The secret whose value is the same HMAC key the controller verifies every doorbell against. */
|
|
6
|
+
doorbellSecret: secretsmanager.ISecret;
|
|
7
|
+
/** Base URL of Evident's own API — non-secret. */
|
|
8
|
+
evidentApiUrl: string;
|
|
9
|
+
/** The pool runner id the report is filed against — non-secret, public UUID. */
|
|
10
|
+
evidentRunnerId: string;
|
|
11
|
+
/** Shape name -> the `Fn::GetAtt LatestActiveImageVersion` published this deploy. */
|
|
12
|
+
imageVersions: Record<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* Shape name -> {@link MicrovmImageBuildInputs}, a STRUCTURED object (never
|
|
15
|
+
* a delimiter-joined string — see its docstring for why flattening is
|
|
16
|
+
* unsafe) that changes whenever THIS deploy's build inputs change. Unlike
|
|
17
|
+
* `imageVersions`' `Fn::GetAtt` (a runtime attribute of the MicroVM image
|
|
18
|
+
* service, whose deploy-time propagation to a dependent resource this
|
|
19
|
+
* construct cannot itself guarantee), every field here is either a
|
|
20
|
+
* synth-time-computed literal or a plain `Ref`/`Fn::GetAtt` to a resource
|
|
21
|
+
* IN THIS STACK — CloudFormation diffs the full resolved property tree, so
|
|
22
|
+
* a change anywhere in this object is a certain, not merely likely,
|
|
23
|
+
* re-invocation trigger.
|
|
24
|
+
*/
|
|
25
|
+
buildTriggers: Record<string, MicrovmImageBuildInputs>;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Deploy-time push: reports each shape's just-published MicroVM image version to
|
|
29
|
+
* Evident, once per deploy that actually changes it. `imageVersions` is the
|
|
30
|
+
* payload; `buildTriggers` is what guarantees re-invocation on exactly the
|
|
31
|
+
* deploy that matters, independent of `imageVersions`' own resolution timing.
|
|
32
|
+
*/
|
|
33
|
+
export declare class ImageVersionReporter extends Construct {
|
|
34
|
+
constructor(scope: Construct, id: string, props: ImageVersionReporterProps);
|
|
35
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ImageVersionReporter = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const cdk = __importStar(require("aws-cdk-lib"));
|
|
39
|
+
const constructs_1 = require("constructs");
|
|
40
|
+
const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
|
|
41
|
+
const cr = __importStar(require("aws-cdk-lib/custom-resources"));
|
|
42
|
+
/** The JSON field name inside the doorbell secret, same key the controller reads. */
|
|
43
|
+
const DOORBELL_SECRET_KEY = 'DOORBELL_SECRET';
|
|
44
|
+
/**
|
|
45
|
+
* Deploy-time push: reports each shape's just-published MicroVM image version to
|
|
46
|
+
* Evident, once per deploy that actually changes it. `imageVersions` is the
|
|
47
|
+
* payload; `buildTriggers` is what guarantees re-invocation on exactly the
|
|
48
|
+
* deploy that matters, independent of `imageVersions`' own resolution timing.
|
|
49
|
+
*/
|
|
50
|
+
class ImageVersionReporter extends constructs_1.Construct {
|
|
51
|
+
constructor(scope, id, props) {
|
|
52
|
+
super(scope, id);
|
|
53
|
+
const handler = new lambda.Function(this, 'Function', {
|
|
54
|
+
runtime: lambda.Runtime.NODEJS_22_X,
|
|
55
|
+
// Pre-bundled at PACKAGE build time, same reasoning as the waker's and the
|
|
56
|
+
// controller's own `Code.fromAsset` (see their comments) — resolved from
|
|
57
|
+
// the PACKAGE ROOT, two directories below this file either compiled
|
|
58
|
+
// (`dist/microvm/image-version-reporter`) or via ts-node
|
|
59
|
+
// (`src/microvm/image-version-reporter`).
|
|
60
|
+
code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', '..', 'dist', 'image-version-reporter-lambda')),
|
|
61
|
+
handler: 'handler.handler',
|
|
62
|
+
memorySize: 128,
|
|
63
|
+
// A deploy hook must not hold CloudFormation for its default 1 h — this is
|
|
64
|
+
// the hard stop; the handler's own internal REPORT_TIMEOUT_MS is well under it.
|
|
65
|
+
timeout: cdk.Duration.seconds(30),
|
|
66
|
+
environment: {
|
|
67
|
+
// ARN + field name only, never the value (mirrors the controller and waker).
|
|
68
|
+
DOORBELL_SECRET_ARN: props.doorbellSecret.secretArn,
|
|
69
|
+
DOORBELL_SECRET_KEY: DOORBELL_SECRET_KEY,
|
|
70
|
+
EVIDENT_API_URL: props.evidentApiUrl,
|
|
71
|
+
EVIDENT_RUNNER_ID: props.evidentRunnerId,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
props.doorbellSecret.grantRead(handler);
|
|
75
|
+
const provider = new cr.Provider(this, 'Provider', {
|
|
76
|
+
onEventHandler: handler,
|
|
77
|
+
});
|
|
78
|
+
new cdk.CustomResource(this, 'Resource', {
|
|
79
|
+
serviceToken: provider.serviceToken,
|
|
80
|
+
properties: {
|
|
81
|
+
imageVersions: props.imageVersions,
|
|
82
|
+
// Not read by the handler — its only job is to be a property value
|
|
83
|
+
// CloudFormation can see change (a structured object, so any field
|
|
84
|
+
// changing anywhere is a real property diff), so the resource is
|
|
85
|
+
// invoked on exactly the deploy that changes a build input.
|
|
86
|
+
buildTriggers: props.buildTriggers,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.ImageVersionReporter = ImageVersionReporter;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ImageVersionPair = {
|
|
2
|
+
shape: string;
|
|
3
|
+
image_version: string;
|
|
4
|
+
};
|
|
5
|
+
export type ReporterEvent = {
|
|
6
|
+
RequestType: 'Create' | 'Update' | 'Delete';
|
|
7
|
+
ResourceProperties?: {
|
|
8
|
+
imageVersions?: unknown;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export type ReporterConfig = {
|
|
12
|
+
doorbellSecretArn: string;
|
|
13
|
+
doorbellSecretKey: string;
|
|
14
|
+
evidentApiUrl: string;
|
|
15
|
+
evidentRunnerId: string;
|
|
16
|
+
};
|
|
17
|
+
type ReporterDependencies = {
|
|
18
|
+
getSecret: (secretArn: string, secretKey: string) => Promise<string>;
|
|
19
|
+
fetch: typeof fetch;
|
|
20
|
+
};
|
|
21
|
+
type ReporterResponse = {
|
|
22
|
+
PhysicalResourceId: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function handleImageVersionReport(event: ReporterEvent, getConfig: () => ReporterConfig, dependencies?: ReporterDependencies): Promise<ReporterResponse>;
|
|
25
|
+
export declare const handler: (event: ReporterEvent) => Promise<ReporterResponse>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handler = void 0;
|
|
4
|
+
exports.handleImageVersionReport = handleImageVersionReport;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
|
|
7
|
+
const PHYSICAL_RESOURCE_ID = 'evident-image-version-report';
|
|
8
|
+
const REPORT_TIMEOUT_MS = 5_000;
|
|
9
|
+
const secrets = new client_secrets_manager_1.SecretsManagerClient({});
|
|
10
|
+
async function fetchDoorbellSecret(secretArn, secretKey) {
|
|
11
|
+
const { SecretString } = await secrets.send(new client_secrets_manager_1.GetSecretValueCommand({ SecretId: secretArn }));
|
|
12
|
+
if (!SecretString) {
|
|
13
|
+
throw new Error(`doorbell secret ${secretArn} has no SecretString`);
|
|
14
|
+
}
|
|
15
|
+
const parsed = JSON.parse(SecretString);
|
|
16
|
+
const value = parsed !== null && typeof parsed === 'object'
|
|
17
|
+
? parsed[secretKey]
|
|
18
|
+
: undefined;
|
|
19
|
+
if (typeof value !== 'string' || value === '') {
|
|
20
|
+
throw new Error(`doorbell secret ${secretArn} is missing a non-empty '${secretKey}' field`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function requireEnv(name) {
|
|
25
|
+
const value = process.env[name];
|
|
26
|
+
if (!value) {
|
|
27
|
+
throw new Error(`missing required env var ${name}`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function readImageVersions(value) {
|
|
32
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
33
|
+
throw new Error('imageVersions must be an object');
|
|
34
|
+
}
|
|
35
|
+
return Object.entries(value).map(([shape, imageVersion]) => {
|
|
36
|
+
if (typeof imageVersion !== 'string') {
|
|
37
|
+
throw new Error(`imageVersions.${shape} must be a string`);
|
|
38
|
+
}
|
|
39
|
+
return { shape, image_version: imageVersion };
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function response() {
|
|
43
|
+
return { PhysicalResourceId: PHYSICAL_RESOURCE_ID };
|
|
44
|
+
}
|
|
45
|
+
const runtimeDependencies = {
|
|
46
|
+
getSecret: fetchDoorbellSecret,
|
|
47
|
+
fetch: globalThis.fetch,
|
|
48
|
+
};
|
|
49
|
+
async function handleImageVersionReport(event, getConfig, dependencies = runtimeDependencies) {
|
|
50
|
+
if (event.RequestType === 'Delete') {
|
|
51
|
+
return response();
|
|
52
|
+
}
|
|
53
|
+
let runnerId = 'unknown';
|
|
54
|
+
let versions = [];
|
|
55
|
+
let responseStatus;
|
|
56
|
+
try {
|
|
57
|
+
versions = readImageVersions(event.ResourceProperties?.imageVersions);
|
|
58
|
+
const config = getConfig();
|
|
59
|
+
runnerId = config.evidentRunnerId;
|
|
60
|
+
const secret = await dependencies.getSecret(config.doorbellSecretArn, config.doorbellSecretKey);
|
|
61
|
+
const body = JSON.stringify({
|
|
62
|
+
type: 'runner.microvm_image_versions_reported',
|
|
63
|
+
versions,
|
|
64
|
+
});
|
|
65
|
+
const signature = (0, node_crypto_1.createHmac)('sha256', secret).update(body).digest('hex');
|
|
66
|
+
const url = `${config.evidentApiUrl.replace(/\/+$/, '')}/v1/runners/${encodeURIComponent(runnerId)}/microvm-image-versions`;
|
|
67
|
+
const result = await dependencies.fetch(url, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: {
|
|
70
|
+
'content-type': 'application/json',
|
|
71
|
+
'x-evident-signature': signature,
|
|
72
|
+
},
|
|
73
|
+
body,
|
|
74
|
+
signal: AbortSignal.timeout(REPORT_TIMEOUT_MS),
|
|
75
|
+
});
|
|
76
|
+
responseStatus = result.status;
|
|
77
|
+
console.log('runner.microvm_image_versions_reported', {
|
|
78
|
+
runnerId,
|
|
79
|
+
versions,
|
|
80
|
+
status: result.status,
|
|
81
|
+
});
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
throw new Error(`Evident returned HTTP ${result.status}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
+
console.warn('MicroVM image version report failed', {
|
|
89
|
+
operation: event.RequestType,
|
|
90
|
+
runnerId,
|
|
91
|
+
versions,
|
|
92
|
+
...(responseStatus === undefined ? {} : { status: responseStatus }),
|
|
93
|
+
error: message,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return response();
|
|
97
|
+
}
|
|
98
|
+
const handler = (event) => handleImageVersionReport(event, () => ({
|
|
99
|
+
doorbellSecretArn: requireEnv('DOORBELL_SECRET_ARN'),
|
|
100
|
+
doorbellSecretKey: requireEnv('DOORBELL_SECRET_KEY'),
|
|
101
|
+
evidentApiUrl: requireEnv('EVIDENT_API_URL'),
|
|
102
|
+
evidentRunnerId: requireEnv('EVIDENT_RUNNER_ID'),
|
|
103
|
+
}));
|
|
104
|
+
exports.handler = handler;
|