@evident-ai/runner-cdk 3.4.1-dev.1856549 → 3.4.1-dev.2b2679a
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 +21 -6
- package/dist/controller-lambda/handler.js +25 -14
- package/dist/evident-scale-to-zero-construct.d.ts +16 -4
- package/dist/evident-scale-to-zero-construct.js +17 -12
- package/dist/image-version-reporter-lambda/handler.js +129 -0
- package/dist/microvm/construct.d.ts +24 -0
- package/dist/microvm/construct.js +32 -1
- package/dist/microvm/controller/doorbell.d.ts +5 -0
- package/dist/microvm/controller/doorbell.js +11 -10
- package/dist/microvm/controller/handle-doorbell.js +17 -14
- package/dist/microvm/controller/microvm-client.d.ts +6 -0
- package/dist/microvm/controller/shape-catalogue.d.ts +1 -1
- package/dist/microvm/controller/shape-catalogue.js +1 -1
- package/dist/microvm/image/stage-context.d.ts +14 -5
- package/dist/microvm/image/stage-context.js +46 -16
- 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 +30 -48
- package/dist/microvm-image-context/hooks/common.sh +205 -718
- package/dist/microvm-image-context/hooks/resume +12 -9
- package/dist/microvm-image-context/hooks/run +18 -39
- package/dist/microvm-image-context/hooks/suspend +4 -2
- package/dist/microvm-image-context/hooks/terminate +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ the construct's own dogfooding consumer.
|
|
|
97
97
|
| `cluster`, `securityGroup`, `taskRole`, `logGroup`, `replicaBucket` | interfaces | Shared infra you create and pass in. |
|
|
98
98
|
| `image` | `ecs.ContainerImage` | Your runner image. |
|
|
99
99
|
| `agentSecret`, `wakerSecret` | `secretsmanager.ISecret` | Any Secrets Manager secret. |
|
|
100
|
-
| `availableSecretKeys` | `Set<string>` | Keys present in the agent secret.
|
|
100
|
+
| `availableSecretKeys` | `Set<string>` | Keys present in the agent secret. Every listed key is injected; `EVIDENT_AGENT_KEY` and `GH_TOKEN` are also injected for compatibility with adopters that do not enumerate their secret. |
|
|
101
101
|
|
|
102
102
|
### Two deliberate "no default" choices
|
|
103
103
|
|
|
@@ -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
|
|
|
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
|
+
workspace build warm-up after dependency installation. See the [MicroVM image
|
|
165
|
+
README](../../runner/docker-images/microvm/README.md) for the full overlay contract.
|
|
166
|
+
|
|
159
167
|
Your repo does not have to be a pnpm workspace. If it commits a `pnpm-lock.yaml` the image
|
|
160
168
|
pre-installs dependencies at build time (a faster first boot); if not, that step is skipped
|
|
161
169
|
and the agent installs on first use.
|
|
162
170
|
|
|
163
171
|
| MicroVM prop | Omitted behavior |
|
|
164
172
|
| --- | --- |
|
|
165
|
-
| `runnerSecret` | No GitHub or MCP credentials are exported at `/run`. |
|
|
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
|
|
|
@@ -42866,7 +42866,7 @@ Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.ht
|
|
|
42866
42866
|
// ../../node_modules/.pnpm/@aws-sdk+signature-v4-multi-region@3.996.42/node_modules/@aws-sdk/signature-v4-multi-region/dist-cjs/index.js
|
|
42867
42867
|
var require_dist_cjs23 = __commonJS({
|
|
42868
42868
|
"../../node_modules/.pnpm/@aws-sdk+signature-v4-multi-region@3.996.42/node_modules/@aws-sdk/signature-v4-multi-region/dist-cjs/index.js"(exports2) {
|
|
42869
|
-
var { SignatureV4: SignatureV43, signatureV4aContainer } =
|
|
42869
|
+
var { SignatureV4: SignatureV43, signatureV4aContainer } = require_dist_cjs2();
|
|
42870
42870
|
var signatureV4CrtContainer = {
|
|
42871
42871
|
CrtSignerV4: null
|
|
42872
42872
|
};
|
|
@@ -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
|
|
@@ -49949,6 +49954,7 @@ function decide({
|
|
|
49949
49954
|
runnerId,
|
|
49950
49955
|
microvmId,
|
|
49951
49956
|
startedAt,
|
|
49957
|
+
imageVersion,
|
|
49952
49958
|
shapes,
|
|
49953
49959
|
polled
|
|
49954
49960
|
}) {
|
|
@@ -49966,6 +49972,7 @@ function decide({
|
|
|
49966
49972
|
reason,
|
|
49967
49973
|
...microvmId === void 0 ? {} : { microvm_id: microvmId },
|
|
49968
49974
|
...startedAt === void 0 ? {} : { microvm_started_at: startedAt.toISOString() },
|
|
49975
|
+
...imageVersion === void 0 ? {} : { image_version: imageVersion },
|
|
49969
49976
|
...shapes === void 0 ? {} : { shapes },
|
|
49970
49977
|
...polled === true ? { polled: true } : {}
|
|
49971
49978
|
}),
|
|
@@ -50122,10 +50129,11 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
50122
50129
|
runnerId: doorbell.runnerId,
|
|
50123
50130
|
microvmId: doorbell.microvmId,
|
|
50124
50131
|
polled,
|
|
50125
|
-
startedAt: described.startedAt
|
|
50132
|
+
startedAt: described.startedAt,
|
|
50133
|
+
imageVersion: described.imageVersion
|
|
50126
50134
|
});
|
|
50127
50135
|
case "SUSPENDED":
|
|
50128
|
-
if (await shouldRecreateForNewerImage(doorbell, shape, described.imageVersion, microvm)) {
|
|
50136
|
+
if (doorbell.recreateOnOutdatedImage === true && await shouldRecreateForNewerImage(doorbell, shape, described.imageVersion, microvm)) {
|
|
50129
50137
|
return runMicrovm(
|
|
50130
50138
|
doorbell,
|
|
50131
50139
|
shape,
|
|
@@ -50143,7 +50151,8 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
50143
50151
|
microvm,
|
|
50144
50152
|
timing,
|
|
50145
50153
|
polled,
|
|
50146
|
-
described.startedAt
|
|
50154
|
+
described.startedAt,
|
|
50155
|
+
described.imageVersion
|
|
50147
50156
|
);
|
|
50148
50157
|
default:
|
|
50149
50158
|
return runMicrovm(
|
|
@@ -50220,10 +50229,11 @@ async function runMicrovm(doorbell, shape, microvm, timing, polled, reason, stat
|
|
|
50220
50229
|
runnerId: doorbell.runnerId,
|
|
50221
50230
|
microvmId: started.microvmId,
|
|
50222
50231
|
polled,
|
|
50223
|
-
startedAt: started.startedAt
|
|
50232
|
+
startedAt: started.startedAt,
|
|
50233
|
+
imageVersion: started.imageVersion
|
|
50224
50234
|
});
|
|
50225
50235
|
}
|
|
50226
|
-
async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled, startedAt) {
|
|
50236
|
+
async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled, startedAt, imageVersion) {
|
|
50227
50237
|
try {
|
|
50228
50238
|
await withThrottleRetry(() => microvm.resume(microvmId), timing);
|
|
50229
50239
|
} catch (error3) {
|
|
@@ -50248,7 +50258,8 @@ async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled
|
|
|
50248
50258
|
runnerId: doorbell.runnerId,
|
|
50249
50259
|
microvmId,
|
|
50250
50260
|
polled,
|
|
50251
|
-
startedAt
|
|
50261
|
+
startedAt,
|
|
50262
|
+
imageVersion
|
|
50252
50263
|
});
|
|
50253
50264
|
}
|
|
50254
50265
|
async function suspendMicrovm(doorbell, microvm, timing) {
|
|
@@ -50443,7 +50454,7 @@ function createRuntimeMicrovm(executionRoleArn) {
|
|
|
50443
50454
|
return latestActiveImageVersion;
|
|
50444
50455
|
},
|
|
50445
50456
|
async run({ imageIdentifier, runHookPayload, clientToken }) {
|
|
50446
|
-
const { microvmId, startedAt } = await microvms.send(
|
|
50457
|
+
const { microvmId, startedAt, imageVersion } = await microvms.send(
|
|
50447
50458
|
new import_client_lambda_microvms.RunMicrovmCommand({
|
|
50448
50459
|
imageIdentifier,
|
|
50449
50460
|
executionRoleArn,
|
|
@@ -50476,7 +50487,7 @@ function createRuntimeMicrovm(executionRoleArn) {
|
|
|
50476
50487
|
if (!microvmId) {
|
|
50477
50488
|
throw new Error("RunMicrovm returned no microvmId");
|
|
50478
50489
|
}
|
|
50479
|
-
return { microvmId, startedAt };
|
|
50490
|
+
return { microvmId, startedAt, imageVersion };
|
|
50480
50491
|
},
|
|
50481
50492
|
async resume(microvmId) {
|
|
50482
50493
|
await microvms.send(new import_client_lambda_microvms.ResumeMicrovmCommand({ microvmIdentifier: microvmId }));
|
|
@@ -61,13 +61,25 @@ export type EvidentScaleToZeroConstructProps = {
|
|
|
61
61
|
replicaBucket: s3.IBucket;
|
|
62
62
|
image: ecs.ContainerImage;
|
|
63
63
|
/**
|
|
64
|
-
* Container secrets.
|
|
65
|
-
*
|
|
66
|
-
*
|
|
64
|
+
* Container secrets. `availableSecretKeys` are injected by name; EVIDENT_AGENT_KEY
|
|
65
|
+
* and GH_TOKEN remain available to external adopters that do not enumerate keys.
|
|
66
|
+
*
|
|
67
|
+
* `agentSecret` always supplies EVIDENT_AGENT_KEY. When `capabilitySecret` is
|
|
68
|
+
* ALSO given (#1868 WI-2), every capability key (GH_TOKEN included) is injected
|
|
69
|
+
* from THAT secret instead, and `availableSecretKeys` is ignored — the two
|
|
70
|
+
* deploy targets (this one and the MicroVM stack) then read the same
|
|
71
|
+
* capability keys from the same shared secret rather than each carrying its
|
|
72
|
+
* own copy. When `capabilitySecret` is absent, behavior is unchanged: GH_TOKEN
|
|
73
|
+
* plus every `availableSecretKeys` entry, all from `agentSecret` — an external
|
|
74
|
+
* adopter passing only `agentSecret`/`availableSecretKeys` is unaffected.
|
|
67
75
|
*/
|
|
68
76
|
agentSecret: secretsmanager.ISecret;
|
|
69
|
-
/** Key names present in the agent secret
|
|
77
|
+
/** Key names present in the agent secret. Every listed key is injected into the container. */
|
|
70
78
|
availableSecretKeys: Set<string>;
|
|
79
|
+
/** The shared capability secret (#1868 WI-2). See `agentSecret`'s doc above. */
|
|
80
|
+
capabilitySecret?: secretsmanager.ISecret;
|
|
81
|
+
/** Key names present in `capabilitySecret`. Ignored when `capabilitySecret` is absent. */
|
|
82
|
+
capabilityKeys?: Set<string>;
|
|
71
83
|
/** Waker secret (EVIDENT_WAKE_SECRET), consumed by the waker at runtime. */
|
|
72
84
|
wakerSecret: secretsmanager.ISecret;
|
|
73
85
|
};
|
|
@@ -61,7 +61,7 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
|
|
|
61
61
|
service;
|
|
62
62
|
constructor(scope, id, props) {
|
|
63
63
|
super(scope, id);
|
|
64
|
-
const { agentName, evidentAgentId, gitRepo, gitBranch, idleTimeoutSeconds, cpu, memoryLimitMiB, resourcePrefix = DEFAULT_RESOURCE_PREFIX, envName, evidentApiUrl, evidentTunnelUrl, extraEnvironment, cluster, securityGroup, taskRole, logGroup, replicaBucket, image, agentSecret, availableSecretKeys, wakerSecret, } = props;
|
|
64
|
+
const { agentName, evidentAgentId, gitRepo, gitBranch, idleTimeoutSeconds, cpu, memoryLimitMiB, resourcePrefix = DEFAULT_RESOURCE_PREFIX, envName, evidentApiUrl, evidentTunnelUrl, extraEnvironment, cluster, securityGroup, taskRole, logGroup, replicaBucket, image, agentSecret, availableSecretKeys, capabilitySecret, capabilityKeys, wakerSecret, } = props;
|
|
65
65
|
this.replicaPrefix = `agents/${evidentAgentId}`;
|
|
66
66
|
// A plain STRING (not service.serviceName) so the container env is static and
|
|
67
67
|
// has no construct-ordering dependency on the service.
|
|
@@ -127,16 +127,22 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
|
|
|
127
127
|
...extraEnvironment,
|
|
128
128
|
};
|
|
129
129
|
const secrets = {
|
|
130
|
+
// Identifies the runner to Evident. Always agentSecret — capabilitySecret
|
|
131
|
+
// never carries this key (#1868 WI-2: the two are a deliberate split, not
|
|
132
|
+
// a duplication of the same data).
|
|
130
133
|
EVIDENT_AGENT_KEY: ecs.Secret.fromSecretsManager(agentSecret, 'EVIDENT_AGENT_KEY'),
|
|
131
|
-
GH_TOKEN: ecs.Secret.fromSecretsManager(agentSecret, 'GH_TOKEN'),
|
|
132
134
|
};
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
if (capabilitySecret !== undefined) {
|
|
136
|
+
for (const key of capabilityKeys ?? new Set()) {
|
|
137
|
+
secrets[key] = ecs.Secret.fromSecretsManager(capabilitySecret, key);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Legacy shape, unchanged: GH_TOKEN (required to clone this construct's
|
|
142
|
+
// workspace at boot) plus every availableSecretKeys entry, all from
|
|
143
|
+
// agentSecret — what every external adopter still gets.
|
|
144
|
+
secrets.GH_TOKEN = ecs.Secret.fromSecretsManager(agentSecret, 'GH_TOKEN');
|
|
145
|
+
for (const key of availableSecretKeys) {
|
|
140
146
|
secrets[key] = ecs.Secret.fromSecretsManager(agentSecret, key);
|
|
141
147
|
}
|
|
142
148
|
}
|
|
@@ -162,9 +168,8 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
|
|
|
162
168
|
cluster,
|
|
163
169
|
taskDefinition,
|
|
164
170
|
serviceName,
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
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.
|
|
168
173
|
enableExecuteCommand: true,
|
|
169
174
|
assignPublicIp: true,
|
|
170
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
|
|
@@ -124,9 +134,14 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
124
134
|
...(props.runnerOpencodeConfigPath
|
|
125
135
|
? { RUNNER_OPENCODE_CONFIG: props.runnerOpencodeConfigPath }
|
|
126
136
|
: {}),
|
|
127
|
-
...(props.runnerSecret
|
|
137
|
+
...(props.runnerSecret
|
|
138
|
+
? {
|
|
139
|
+
RUNNER_SECRET_ARN: props.runnerSecret.secretArn,
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
128
142
|
...(props.gitUserName ? { GIT_USER_NAME: props.gitUserName } : {}),
|
|
129
143
|
...(props.gitUserEmail ? { GIT_USER_EMAIL: props.gitUserEmail } : {}),
|
|
144
|
+
...props.extraImageEnvironment,
|
|
130
145
|
};
|
|
131
146
|
// ...and the hooks configuration every shape's image is built with.
|
|
132
147
|
const imageHooks = {
|
|
@@ -245,6 +260,22 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
245
260
|
// HMAC verification of the doorbell body IS the auth, as for the ECS waker.
|
|
246
261
|
authType: lambda.FunctionUrlAuthType.NONE,
|
|
247
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
|
+
}
|
|
248
279
|
this.functionUrl = functionUrl.url;
|
|
249
280
|
this.shapeCatalogueJson = shapeCatalogueJson;
|
|
250
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
|
}
|
|
@@ -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
|
}),
|
|
@@ -128,9 +129,8 @@ function compareImageVersions(a, b) {
|
|
|
128
129
|
return 0;
|
|
129
130
|
}
|
|
130
131
|
/**
|
|
131
|
-
* Whether
|
|
132
|
-
* image has been published since it booted
|
|
133
|
-
* 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.
|
|
134
134
|
*
|
|
135
135
|
* FAIL-SAFE, one direction only: recreating destroys the VM's filesystem
|
|
136
136
|
* (only the credentials and `opencode.db` in the object store survive), so
|
|
@@ -283,16 +283,18 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
283
283
|
microvmId: doorbell.microvmId,
|
|
284
284
|
polled,
|
|
285
285
|
startedAt: described.startedAt,
|
|
286
|
+
imageVersion: described.imageVersion,
|
|
286
287
|
});
|
|
287
288
|
case 'SUSPENDED':
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
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))) {
|
|
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>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses the `MICROVM_SHAPES` environment variable (a JSON array, written by
|
|
3
|
-
* CloudFormation from `
|
|
3
|
+
* CloudFormation from `aws/runner-cdk/src/microvm/shapes.ts` at
|
|
4
4
|
* deploy time — see D6/D7 in the plan) into a `ShapeCatalogue` the pure
|
|
5
5
|
* decision core can query, without that core ever touching `process.env`
|
|
6
6
|
* itself.
|