@evident-ai/runner-cdk 3.4.1-dev.c463782 → 3.4.1-dev.d0469f4
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 +26 -12
- package/dist/controller-lambda/handler.js +11 -6
- package/dist/evident-scale-to-zero-construct.d.ts +1 -1
- package/dist/evident-scale-to-zero-construct.js +2 -3
- package/dist/image-version-reporter-lambda/handler.js +129 -0
- package/dist/microvm/construct.d.ts +24 -0
- package/dist/microvm/construct.js +27 -0
- package/dist/microvm/controller/doorbell.d.ts +5 -0
- package/dist/microvm/controller/doorbell.js +11 -10
- package/dist/microvm/controller/handle-doorbell.js +8 -8
- package/dist/microvm/image/stage-context.d.ts +14 -5
- package/dist/microvm/image/stage-context.js +47 -20
- 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/Dockerfile +28 -65
- package/dist/microvm-image-context/hooks/common.sh +121 -173
- package/dist/microvm-image-context/hooks/resume +11 -8
- package/dist/microvm-image-context/hooks/run +8 -7
- package/dist/microvm-image-context/hooks/suspend +4 -5
- package/dist/microvm-image-context/hooks/terminate +5 -6
- package/dist/waker/construct.js +1 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Reusable AWS CDK constructs for running an [Evident](https://evident.run) agent
|
|
|
11
11
|
- **`EvidentMicrovmConstruct`** — a per-session AWS Lambda MicroVM that boots on demand
|
|
12
12
|
and suspends between messages. Installing the `@dev` tag of this package is all you
|
|
13
13
|
need: the image build context ships inside it (see [The MicroVM image](#the-microvm-image)
|
|
14
|
-
below), so there is no checkout of
|
|
14
|
+
below), so there is no checkout of the source repository involved. See
|
|
15
15
|
[the AWS runner doc](https://evident.run/docs/aws-runner) for the full picture of both
|
|
16
16
|
strategies.
|
|
17
17
|
|
|
@@ -76,8 +76,8 @@ agent.selfStopRoleArn;
|
|
|
76
76
|
agent.replicaPrefix;
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
For a complete working example, see `
|
|
80
|
-
the construct's own dogfooding consumer.
|
|
79
|
+
For a complete working example, see [`GETTING-STARTED.md`](./GETTING-STARTED.md) in this
|
|
80
|
+
package — it is the construct's own dogfooding consumer.
|
|
81
81
|
|
|
82
82
|
## Props
|
|
83
83
|
|
|
@@ -121,10 +121,11 @@ context, which `cdk deploy` zips and uploads. The Lambda MicroVM service then bu
|
|
|
121
121
|
image in *your* account, from a base image ARN you discover there — so, unlike the Fargate
|
|
122
122
|
strategy, there is no registry image to pull.
|
|
123
123
|
|
|
124
|
-
The context has
|
|
125
|
-
|
|
126
|
-
`dist/microvm-image-context
|
|
127
|
-
|
|
124
|
+
The context has three parts: this package supplies the Dockerfile, the per-phase hook
|
|
125
|
+
scripts and the bundled hook server, published inside the tarball at
|
|
126
|
+
`dist/microvm-image-context/`; **your** repository is baked in as the agent's workspace;
|
|
127
|
+
and an optional overlay supplies deployment-specific build steps. `stageMicrovmImageContext()`
|
|
128
|
+
puts them together:
|
|
128
129
|
|
|
129
130
|
```ts
|
|
130
131
|
import path from 'node:path';
|
|
@@ -143,6 +144,8 @@ new EvidentMicrovmConstruct(this, 'Runner', {
|
|
|
143
144
|
// token the agent pushes with arrives per-session, never baked in.
|
|
144
145
|
originUrl: 'https://github.com/acme/widgets.git',
|
|
145
146
|
destination: path.join(__dirname, '..', 'build', 'image'),
|
|
147
|
+
// Optional deployment-specific installs and build warm-up.
|
|
148
|
+
overlayDir: '/path/to/microvm-overlay',
|
|
146
149
|
}),
|
|
147
150
|
// Must match the published image these were baked into, so export them
|
|
148
151
|
// rather than restating the numbers.
|
|
@@ -156,15 +159,27 @@ new EvidentMicrovmConstruct(this, 'Runner', {
|
|
|
156
159
|
});
|
|
157
160
|
```
|
|
158
161
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
and
|
|
162
|
+
Pass `overlayDir` to add deployment-specific build steps to the staged image. The
|
|
163
|
+
directory provides `setup-root` for root-owned installs and `setup-workspace` for
|
|
164
|
+
dependency installation and workspace build warm-up. See the [MicroVM image
|
|
165
|
+
README](../../runner/docker-images/microvm/README.md) for the full overlay contract.
|
|
166
|
+
|
|
167
|
+
The image bakes your repository checked out but not installed and makes no assumptions about
|
|
168
|
+
its package manager. To speed up first boot, install dependencies from your overlay's
|
|
169
|
+
`setup-workspace`; otherwise, the agent installs them on first use.
|
|
162
170
|
|
|
163
171
|
| MicroVM prop | Omitted behavior |
|
|
164
172
|
| --- | --- |
|
|
165
173
|
| `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
174
|
| `runnerOpencodeConfigPath` | OpenCode uses the baked project configuration. Relative paths resolve from the workspace; absolute paths resolve in the image. |
|
|
175
|
+
| `extraImageEnvironment` | Non-secret values baked into every VM launched from this image version; every VM can read them, so never put secrets here. |
|
|
167
176
|
| `gitUserName` / `gitUserEmail` | The hook uses the Evident bot defaults for git identity. |
|
|
177
|
+
| `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. |
|
|
178
|
+
|
|
179
|
+
`extraImageEnvironment` is the runtime counterpart to an overlay. Use it when an
|
|
180
|
+
overlay-installed component needs an environment variable after the VM boots. The
|
|
181
|
+
values are baked into every VM launched from that image version and are readable by
|
|
182
|
+
all of them, so never put a secret in this prop.
|
|
168
183
|
|
|
169
184
|
## Status / limitations
|
|
170
185
|
|
|
@@ -172,7 +187,6 @@ and the agent installs on first use.
|
|
|
172
187
|
`npm install @evident-ai/runner-cdk@dev` (or `pnpm add`/`yarn add`); no checkout of
|
|
173
188
|
this repo is required. It builds to a self-contained `dist/` (`pnpm --filter
|
|
174
189
|
@evident-ai/runner-cdk build`) with no `workspace:`/`@evident/*` runtime dependency.
|
|
175
|
-
|
|
176
|
-
`workspace:*` dependency for dogfooding.
|
|
190
|
+
Evident's own consumer still consumes it as a `workspace:*` dependency for dogfooding.
|
|
177
191
|
- **AWS/ECS-specific.** Non-AWS clouds are an open question on the epic, not a supported
|
|
178
192
|
path.
|
|
@@ -49854,7 +49854,8 @@ function parseDoorbell(rawBody) {
|
|
|
49854
49854
|
occurred_at: occurredAt,
|
|
49855
49855
|
microvm_id: microvmId,
|
|
49856
49856
|
run_payload: runPayload,
|
|
49857
|
-
shape
|
|
49857
|
+
shape,
|
|
49858
|
+
recreate_on_outdated_image: recreateOnOutdatedImage
|
|
49858
49859
|
} = body;
|
|
49859
49860
|
if (type !== SUSPEND_EVENT_TYPE && type !== SHAPES_EVENT_TYPE && !isWakeType(type)) {
|
|
49860
49861
|
return { ok: false, reason: DOORBELL_REJECTION.unsupportedType };
|
|
@@ -49885,10 +49886,14 @@ function parseDoorbell(rawBody) {
|
|
|
49885
49886
|
if (Buffer.byteLength(runHookPayload, "utf8") > RUN_HOOK_PAYLOAD_MAX_BYTES) {
|
|
49886
49887
|
return { ok: false, reason: DOORBELL_REJECTION.runPayloadTooLarge };
|
|
49887
49888
|
}
|
|
49888
|
-
|
|
49889
|
-
|
|
49890
|
-
doorbell
|
|
49891
|
-
}
|
|
49889
|
+
const doorbell = { ...fields, type, runHookPayload };
|
|
49890
|
+
if (shape !== void 0) {
|
|
49891
|
+
doorbell.shape = shape;
|
|
49892
|
+
}
|
|
49893
|
+
if (recreateOnOutdatedImage === true) {
|
|
49894
|
+
doorbell.recreateOnOutdatedImage = true;
|
|
49895
|
+
}
|
|
49896
|
+
return { ok: true, doorbell };
|
|
49892
49897
|
}
|
|
49893
49898
|
|
|
49894
49899
|
// src/microvm/controller/throttle-retry.ts
|
|
@@ -50128,7 +50133,7 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
50128
50133
|
imageVersion: described.imageVersion
|
|
50129
50134
|
});
|
|
50130
50135
|
case "SUSPENDED":
|
|
50131
|
-
if (await shouldRecreateForNewerImage(doorbell, shape, described.imageVersion, microvm)) {
|
|
50136
|
+
if (doorbell.recreateOnOutdatedImage === true && await shouldRecreateForNewerImage(doorbell, shape, described.imageVersion, microvm)) {
|
|
50132
50137
|
return runMicrovm(
|
|
50133
50138
|
doorbell,
|
|
50134
50139
|
shape,
|
|
@@ -17,7 +17,7 @@ export type EvidentScaleToZeroConstructProps = {
|
|
|
17
17
|
* (`agents/<id>`) and is the single expected `agent_id` the waker checks.
|
|
18
18
|
*/
|
|
19
19
|
evidentAgentId: string;
|
|
20
|
-
/** GitHub `owner/repo` the agent clones and works on (e.g. `
|
|
20
|
+
/** GitHub `owner/repo` the agent clones and works on (e.g. `my-org/my-repo`). */
|
|
21
21
|
gitRepo: string;
|
|
22
22
|
/** Branch to clone. Defaults to `main` when omitted. */
|
|
23
23
|
gitBranch?: string;
|
|
@@ -168,9 +168,8 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
|
|
|
168
168
|
cluster,
|
|
169
169
|
taskDefinition,
|
|
170
170
|
serviceName,
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
desiredCount: 1,
|
|
171
|
+
// Deliberately omit DesiredCount: CloudFormation leaves the existing service's
|
|
172
|
+
// self-stop/waker count alone; either 1 or 0 would re-assert it on every deploy.
|
|
174
173
|
enableExecuteCommand: true,
|
|
175
174
|
assignPublicIp: true,
|
|
176
175
|
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
|
|
@@ -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
|
+
});
|
|
@@ -55,6 +55,12 @@ export interface EvidentMicrovmConstructProps {
|
|
|
55
55
|
readonly gitUserName?: string;
|
|
56
56
|
/** Optional git email the `/run` hook configures for agent commits. */
|
|
57
57
|
readonly gitUserEmail?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Non-secret values baked into every VM launched from this image version,
|
|
60
|
+
* typically values a deployment's overlay needs at runtime. Every VM can
|
|
61
|
+
* read them, so this must never contain a secret.
|
|
62
|
+
*/
|
|
63
|
+
readonly extraImageEnvironment?: Record<string, string>;
|
|
58
64
|
/**
|
|
59
65
|
* TCP port the image's hook server listens on, baked into both the image
|
|
60
66
|
* (`HOOKS_PORT` env var) and the `MicrovmImage`'s `hooks.port` — a mismatch
|
|
@@ -69,6 +75,24 @@ export interface EvidentMicrovmConstructProps {
|
|
|
69
75
|
* running a hook).
|
|
70
76
|
*/
|
|
71
77
|
readonly hookTimeoutSeconds: number;
|
|
78
|
+
/**
|
|
79
|
+
* Base URL of Evident's own API. Together with `evidentRunnerId`, opts this
|
|
80
|
+
* deploy into reporting each shape's published image version to Evident
|
|
81
|
+
* (#1903) — a CDK custom resource, triggered only when a deploy actually
|
|
82
|
+
* changes a version. Omitting BOTH leaves the template byte-for-byte
|
|
83
|
+
* identical to not having this prop at all: an adopter with no Evident
|
|
84
|
+
* runner id configured has nothing to report against. Supplying only one
|
|
85
|
+
* throws at synth — see `evidentRunnerId`.
|
|
86
|
+
*/
|
|
87
|
+
readonly evidentApiUrl?: string;
|
|
88
|
+
/**
|
|
89
|
+
* The pool runner id (public, non-secret UUID) to report against — see
|
|
90
|
+
* `evidentApiUrl`. Mirrors `EvidentScaleToZeroConstructProps.evidentAgentId`'s
|
|
91
|
+
* "public (non-secret) Evident agent UUID" precedent. Must be supplied
|
|
92
|
+
* together with `evidentApiUrl`, or neither at all — supplying exactly one
|
|
93
|
+
* throws at construction rather than silently disabling the reporter.
|
|
94
|
+
*/
|
|
95
|
+
readonly evidentRunnerId?: string;
|
|
72
96
|
}
|
|
73
97
|
/**
|
|
74
98
|
* 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
|
|
@@ -131,6 +141,7 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
131
141
|
: {}),
|
|
132
142
|
...(props.gitUserName ? { GIT_USER_NAME: props.gitUserName } : {}),
|
|
133
143
|
...(props.gitUserEmail ? { GIT_USER_EMAIL: props.gitUserEmail } : {}),
|
|
144
|
+
...props.extraImageEnvironment,
|
|
134
145
|
};
|
|
135
146
|
// ...and the hooks configuration every shape's image is built with.
|
|
136
147
|
const imageHooks = {
|
|
@@ -249,6 +260,22 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
249
260
|
// HMAC verification of the doorbell body IS the auth, as for the ECS waker.
|
|
250
261
|
authType: lambda.FunctionUrlAuthType.NONE,
|
|
251
262
|
});
|
|
263
|
+
// Opt-in only (D8): both values present is what creates the reporter, so an
|
|
264
|
+
// adopter who supplies neither gets a template with no additional Lambda and
|
|
265
|
+
// no custom resource — this branch must not run for them.
|
|
266
|
+
if (props.evidentApiUrl && props.evidentRunnerId) {
|
|
267
|
+
new construct_1.ImageVersionReporter(this, 'ImageVersionReporter', {
|
|
268
|
+
doorbellSecret: props.doorbellSecret,
|
|
269
|
+
evidentApiUrl: props.evidentApiUrl,
|
|
270
|
+
evidentRunnerId: props.evidentRunnerId,
|
|
271
|
+
imageVersions: Object.fromEntries(shapes.map((shape) => [shape.name, images.get(shape.name).latestActiveImageVersion])),
|
|
272
|
+
// `buildInputs` already covers every property that can cause AWS
|
|
273
|
+
// to publish a new version for THIS shape's image (source, base
|
|
274
|
+
// image, memory, hooks, environment, description, build role —
|
|
275
|
+
// see its docstring); no additional encoding needed here.
|
|
276
|
+
buildTriggers: Object.fromEntries(shapes.map((shape) => [shape.name, images.get(shape.name).buildInputs])),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
252
279
|
this.functionUrl = functionUrl.url;
|
|
253
280
|
this.shapeCatalogueJson = shapeCatalogueJson;
|
|
254
281
|
this.durableStateBucket = durableState;
|
|
@@ -40,6 +40,11 @@ export type WakeDoorbell = DoorbellFields & {
|
|
|
40
40
|
* shape" — the controller resolves it against the shape catalogue.
|
|
41
41
|
*/
|
|
42
42
|
shape?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Set only when the body carries the literal `true`. When absent, the
|
|
45
|
+
* suspended VM is always resumed because recreating it destroys its filesystem.
|
|
46
|
+
*/
|
|
47
|
+
recreateOnOutdatedImage?: boolean;
|
|
43
48
|
};
|
|
44
49
|
export type Doorbell = WakeDoorbell | (DoorbellFields & {
|
|
45
50
|
type: typeof SUSPEND_EVENT_TYPE;
|
|
@@ -63,7 +63,7 @@ function parseDoorbell(rawBody) {
|
|
|
63
63
|
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
64
64
|
return { ok: false, reason: exports.DOORBELL_REJECTION.invalidJson };
|
|
65
65
|
}
|
|
66
|
-
const { type, runner_id: runnerId, occurred_at: occurredAt, microvm_id: microvmId, run_payload: runPayload, shape, } = body;
|
|
66
|
+
const { type, runner_id: runnerId, occurred_at: occurredAt, microvm_id: microvmId, run_payload: runPayload, shape, recreate_on_outdated_image: recreateOnOutdatedImage, } = body;
|
|
67
67
|
if (type !== SUSPEND_EVENT_TYPE && type !== exports.SHAPES_EVENT_TYPE && !isWakeType(type)) {
|
|
68
68
|
return { ok: false, reason: exports.DOORBELL_REJECTION.unsupportedType };
|
|
69
69
|
}
|
|
@@ -95,13 +95,14 @@ function parseDoorbell(rawBody) {
|
|
|
95
95
|
if (Buffer.byteLength(runHookPayload, 'utf8') > constants_1.RUN_HOOK_PAYLOAD_MAX_BYTES) {
|
|
96
96
|
return { ok: false, reason: exports.DOORBELL_REJECTION.runPayloadTooLarge };
|
|
97
97
|
}
|
|
98
|
-
// Omit
|
|
99
|
-
// so
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
98
|
+
// Omit optional fields entirely when absent, rather than setting them to
|
|
99
|
+
// `undefined`, so older bodies keep the same parsed shape.
|
|
100
|
+
const doorbell = { ...fields, type, runHookPayload };
|
|
101
|
+
if (shape !== undefined) {
|
|
102
|
+
doorbell.shape = shape;
|
|
103
|
+
}
|
|
104
|
+
if (recreateOnOutdatedImage === true) {
|
|
105
|
+
doorbell.recreateOnOutdatedImage = true;
|
|
106
|
+
}
|
|
107
|
+
return { ok: true, doorbell };
|
|
107
108
|
}
|
|
@@ -129,9 +129,8 @@ function compareImageVersions(a, b) {
|
|
|
129
129
|
return 0;
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
|
-
* Whether
|
|
133
|
-
* image has been published since it booted
|
|
134
|
-
* that VM on its old baked-in hooks and repo checkout for up to 8 hours.
|
|
132
|
+
* Whether an opted-in doorbell should throw away and recreate the SUSPENDED VM
|
|
133
|
+
* because a newer image has been published since it booted.
|
|
135
134
|
*
|
|
136
135
|
* FAIL-SAFE, one direction only: recreating destroys the VM's filesystem
|
|
137
136
|
* (only the credentials and `opencode.db` in the object store survive), so
|
|
@@ -287,11 +286,12 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
287
286
|
imageVersion: described.imageVersion,
|
|
288
287
|
});
|
|
289
288
|
case 'SUSPENDED':
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
if (
|
|
289
|
+
// False/absent disables silent automatic roll-forward; an explicit restart
|
|
290
|
+
// request (#1906) can still set the same wire flag. Recreate loses VM-local
|
|
291
|
+
// filesystem state; durable provider credentials survive in object storage,
|
|
292
|
+
// but a manual paste/upload not yet synced there may be lost (#2071).
|
|
293
|
+
if (doorbell.recreateOnOutdatedImage === true &&
|
|
294
|
+
(await shouldRecreateForNewerImage(doorbell, shape, described.imageVersion, microvm))) {
|
|
295
295
|
return runMicrovm(doorbell, shape, microvm, timing, polled, REASON.imageVersionOutdated, state);
|
|
296
296
|
}
|
|
297
297
|
return resumeMicrovm(doorbell, doorbell.microvmId, shape, microvm, timing, polled, described.startedAt, described.imageVersion);
|
|
@@ -14,6 +14,12 @@ export interface StageMicrovmImageContextOptions {
|
|
|
14
14
|
readonly originUrl: string;
|
|
15
15
|
/** Directory to write the build context to. Removed and recreated. */
|
|
16
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;
|
|
17
23
|
/**
|
|
18
24
|
* The published template to copy from. Defaults to the one inside this
|
|
19
25
|
* package; named directly by the tests, which assert against a template they
|
|
@@ -23,11 +29,14 @@ export interface StageMicrovmImageContextOptions {
|
|
|
23
29
|
}
|
|
24
30
|
/**
|
|
25
31
|
* Writes the build context AWS unpacks — the Dockerfile, the hook server, the
|
|
26
|
-
* per-phase hook scripts and the repository — and
|
|
27
|
-
* is what `EvidentMicrovmConstruct`'s `imageSource`
|
|
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.
|
|
28
35
|
*
|
|
29
|
-
* The
|
|
30
|
-
*
|
|
31
|
-
*
|
|
36
|
+
* The Dockerfile, hook server and phase hooks come from this package's
|
|
37
|
+
* published `dist/`, so a consumer needs no checkout of the source repository and no
|
|
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.
|
|
32
41
|
*/
|
|
33
42
|
export declare function stageMicrovmImageContext(options: StageMicrovmImageContextOptions): string;
|
|
@@ -66,8 +66,8 @@ function isShallowRepository(repositoryPath) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Clones the caller's repository into the build context
|
|
70
|
-
*
|
|
69
|
+
* Clones the caller's repository into the build context for the Dockerfile to
|
|
70
|
+
* bake as a checked-out but uninstalled workspace.
|
|
71
71
|
*
|
|
72
72
|
* `file://` rather than a plain path: a path triggers git's local-clone
|
|
73
73
|
* optimisation, which copies the whole object store — every branch and worktree
|
|
@@ -102,21 +102,37 @@ function stageRepository(repositoryPath, destination, originUrl) {
|
|
|
102
102
|
(0, node_fs_1.rmSync)(path.join(destination, '.git', 'index'), { force: true });
|
|
103
103
|
(0, node_fs_1.rmSync)(path.join(destination, '.git', 'logs'), { recursive: true, force: true });
|
|
104
104
|
console.log(`[stage] repo ${git(['rev-parse', 'HEAD'], destination)}`);
|
|
105
|
-
|
|
106
|
-
|
|
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);
|
|
107
120
|
}
|
|
108
121
|
}
|
|
109
122
|
/**
|
|
110
123
|
* Writes the build context AWS unpacks — the Dockerfile, the hook server, the
|
|
111
|
-
* per-phase hook scripts and the repository — and
|
|
112
|
-
* is what `EvidentMicrovmConstruct`'s `imageSource`
|
|
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.
|
|
113
127
|
*
|
|
114
|
-
* The
|
|
115
|
-
*
|
|
116
|
-
*
|
|
128
|
+
* The Dockerfile, hook server and phase hooks come from this package's
|
|
129
|
+
* published `dist/`, so a consumer needs no checkout of the source repository and no
|
|
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.
|
|
117
133
|
*/
|
|
118
134
|
function stageMicrovmImageContext(options) {
|
|
119
|
-
const { originUrl, templateDir = TEMPLATE_DIR } = options;
|
|
135
|
+
const { originUrl, overlayDir, templateDir = TEMPLATE_DIR } = options;
|
|
120
136
|
// `file://` and the clone below only mean anything against absolute paths,
|
|
121
137
|
// and a caller may reasonably pass either.
|
|
122
138
|
const repositoryPath = path.resolve(options.repositoryPath);
|
|
@@ -132,16 +148,27 @@ function stageMicrovmImageContext(options) {
|
|
|
132
148
|
(0, node_fs_1.copyFileSync)(path.join(templateDir, 'hook-server.js'), path.join(destination, 'hook-server.js'));
|
|
133
149
|
const hooksSource = path.join(templateDir, 'hooks');
|
|
134
150
|
const hooksStage = path.join(destination, 'hooks');
|
|
135
|
-
(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
(0, node_fs_1.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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);
|
|
145
172
|
}
|
|
146
173
|
stageRepository(repositoryPath, path.join(destination, 'repo'), originUrl);
|
|
147
174
|
return destination;
|
|
@@ -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
|
+
}
|