@evident-ai/runner-cdk 3.4.1-dev.59c7df3 → 3.4.1-dev.6f7da9c
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 +24 -13
- 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 +17 -14
- package/dist/microvm/controller/microvm-client.d.ts +6 -0
- 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
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Generic MicroVM runner image
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# Built for linux/arm64 (Lambda MicroVMs run on Graviton only) and with ONE
|
|
4
|
+
# process: the hook server. `overlay/setup-root` and `overlay/setup-workspace`
|
|
5
|
+
# are the extension point for deployment-specific packages, dependency
|
|
6
|
+
# installation and build warm-up.
|
|
5
7
|
#
|
|
6
8
|
# THE BOOT SPLIT — the rule that governs everything below. AWS snapshots this
|
|
7
9
|
# image ONCE at build and every MicroVM resumes from that same snapshot, so
|
|
@@ -9,7 +11,8 @@
|
|
|
9
11
|
# this image version. Therefore this image must NOT start the tunnel
|
|
10
12
|
# (`evident run`), clone the repo, or carry any credential, machine ID or
|
|
11
13
|
# generated secret. Identity arrives per-VM in the `/run` hook payload.
|
|
12
|
-
# `src/image/dockerfile.test.ts` fails the build if this
|
|
14
|
+
# `aws/runner-cdk/src/microvm/image/dockerfile.test.ts` fails the build if this
|
|
15
|
+
# regresses.
|
|
13
16
|
#
|
|
14
17
|
# No `--platform` on FROM: AWS builds this natively on Graviton. Build it
|
|
15
18
|
# locally with `docker build --platform linux/arm64`.
|
|
@@ -27,9 +30,8 @@ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
|
|
27
30
|
|
|
28
31
|
# System tooling + GitHub CLI + AWS CLI, in a single layer.
|
|
29
32
|
# - awscli (v2): required — durable-state persistence uses `aws s3`.
|
|
30
|
-
# - procps
|
|
31
|
-
#
|
|
32
|
-
# no sudo). Listed explicitly so apt keeps them.
|
|
33
|
+
# - procps: required at RUNTIME for process inspection and debugging. Listed
|
|
34
|
+
# explicitly so apt keeps it.
|
|
33
35
|
# - gnupg, unzip: BUILD-ONLY (gh apt key / AWS CLI bundle); purged in this layer.
|
|
34
36
|
#
|
|
35
37
|
# Purge build-only packages BY NAME — do NOT use `apt-get autoremove`, which
|
|
@@ -38,7 +40,6 @@ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
|
|
38
40
|
RUN apt-get update \
|
|
39
41
|
&& apt-get install -y --no-install-recommends \
|
|
40
42
|
ca-certificates curl git jq less procps psmisc ripgrep \
|
|
41
|
-
postgresql postgresql-contrib direnv \
|
|
42
43
|
gnupg unzip \
|
|
43
44
|
# --- GitHub CLI (`gh`) from the official apt repo ---
|
|
44
45
|
&& mkdir -p -m 755 /etc/apt/keyrings \
|
|
@@ -54,10 +55,6 @@ RUN apt-get update \
|
|
|
54
55
|
&& unzip -q /tmp/awscliv2.zip -d /tmp \
|
|
55
56
|
&& /tmp/aws/install \
|
|
56
57
|
&& aws --version \
|
|
57
|
-
# --- Put the PostgreSQL server binaries on PATH ---
|
|
58
|
-
# On Debian initdb/pg_ctl/postgres/psql live under /usr/lib/postgresql/
|
|
59
|
-
# <ver>/bin, not on PATH; symlink them into /usr/local/bin.
|
|
60
|
-
&& ln -s /usr/lib/postgresql/*/bin/* /usr/local/bin/ 2>/dev/null || true \
|
|
61
58
|
# --- Drop the build-only packages (BY NAME, no autoremove) + caches ---
|
|
62
59
|
&& apt-get purge -y gnupg unzip \
|
|
63
60
|
&& rm -rf /tmp/aws /tmp/awscliv2.zip /var/lib/apt/lists/*
|
|
@@ -87,7 +84,7 @@ RUN curl -fsSL -o /tmp/litestream.tar.gz \
|
|
|
87
84
|
# floating `opencode-ai@latest` install was implicated in unbounded runner
|
|
88
85
|
# memory growth / OOM kills. Keep it in lockstep with the ECS runner image's.
|
|
89
86
|
# EVIDENT_CLI_VERSION is DELIBERATELY the floating `dev` tag, matching the ECS
|
|
90
|
-
# runner image
|
|
87
|
+
# runner image. This is a
|
|
91
88
|
# development environment: it should track the head of the CLI so changes are
|
|
92
89
|
# testable without a version bump in every PR that touches a hook.
|
|
93
90
|
# RUNNER_SYNCHRONISER_VERSION floats on `dev` for the identical reason.
|
|
@@ -104,27 +101,10 @@ RUN npm install -g \
|
|
|
104
101
|
&& evident --version \
|
|
105
102
|
&& npm cache clean --force
|
|
106
103
|
|
|
107
|
-
# Playwright chromium + OS deps for E2E, baked at build time to avoid a slow
|
|
108
|
-
# first-boot download.
|
|
109
|
-
#
|
|
110
|
-
# CRITICAL: this version MUST match the Playwright version the repo resolves for
|
|
111
|
-
# `@playwright/test` (pnpm-lock.yaml → 1.58.0). The browser build is coupled to
|
|
112
|
-
# the package version; if it drifts, a boot-time `playwright install`
|
|
113
|
-
# re-downloads a different chromium. Bump in lockstep with @playwright/test.
|
|
114
|
-
#
|
|
115
|
-
# Browsers go to a SHARED PLAYWRIGHT_BROWSERS_PATH readable by the runtime
|
|
116
|
-
# runner user (uid 10001), not root's ~/.cache.
|
|
117
|
-
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright
|
|
118
|
-
RUN npx playwright@1.58.0 install --with-deps chromium \
|
|
119
|
-
&& chown -R 10001:10001 /opt/ms-playwright \
|
|
120
|
-
# Assert the browsers landed in the shared path (guard against a silent failure).
|
|
121
|
-
&& test -n "$(ls -A /opt/ms-playwright 2>/dev/null)" \
|
|
122
|
-
|| { echo "FATAL: Playwright browsers dir /opt/ms-playwright missing or empty after install" >&2; exit 1; }
|
|
123
|
-
|
|
124
104
|
# Assert every REQUIRED runtime binary survived every layer above (fail loud).
|
|
125
105
|
# Placed after the last install so it covers the apt purge, the litestream
|
|
126
106
|
# extraction and the global npm installs in one pass.
|
|
127
|
-
RUN for bin in ps pgrep pkill curl aws gh git jq rg
|
|
107
|
+
RUN for bin in ps pgrep pkill curl aws gh git jq rg litestream runner-synchroniser opencode evident timeout; do \
|
|
128
108
|
command -v "$bin" >/dev/null 2>&1 || { echo "FATAL: required binary '$bin' missing from image" >&2; exit 1; }; \
|
|
129
109
|
done \
|
|
130
110
|
&& curl -fsS -o /dev/null https://cli.github.com \
|
|
@@ -150,8 +130,8 @@ COPY hook-server.js /usr/local/lib/evident/hook-server.js
|
|
|
150
130
|
COPY hooks /etc/evident/hooks
|
|
151
131
|
|
|
152
132
|
# Workspace: the repository the agent works on, baked in. A resumed VM must be
|
|
153
|
-
# useful in milliseconds,
|
|
154
|
-
# here
|
|
133
|
+
# useful in milliseconds, while a clone costs minutes — so the checkout happens
|
|
134
|
+
# here; dependency installation and build warm-up belong to the deployment overlay.
|
|
155
135
|
#
|
|
156
136
|
# This does not weaken the boot split: source, git history and node_modules are
|
|
157
137
|
# neither secret nor per-VM-unique, so sharing them across every VM from this
|
|
@@ -162,8 +142,6 @@ COPY hooks /etc/evident/hooks
|
|
|
162
142
|
# per-VM, so the agent is what brings this checkout up to date.
|
|
163
143
|
ENV WORKSPACE=/workspace
|
|
164
144
|
ENV HOME=/home/runner
|
|
165
|
-
# Runner-owned PGDATA parent for the local dev Postgres cluster (initdb +
|
|
166
|
-
# pg_ctl at boot, no sudo).
|
|
167
145
|
#
|
|
168
146
|
# The machine-id files are created EMPTY and runner-owned so that /run can
|
|
169
147
|
# actually rewrite them: the hooks run as uid 10001, and neither `/etc` nor
|
|
@@ -171,10 +149,18 @@ ENV HOME=/home/runner
|
|
|
171
149
|
# permanent no-op and every VM from this snapshot shares one machine id. Empty
|
|
172
150
|
# is the correct unset state — it is the absence of an identity, so nothing
|
|
173
151
|
# per-VM-unique enters the shared snapshot.
|
|
174
|
-
RUN mkdir -p /var/lib/
|
|
152
|
+
RUN mkdir -p /var/lib/dbus /home/runner/.local/state/evident \
|
|
175
153
|
&& install -o runner -g runner -m 0644 /dev/null /etc/machine-id \
|
|
176
154
|
&& install -o runner -g runner -m 0644 /dev/null /var/lib/dbus/machine-id \
|
|
177
|
-
|
|
155
|
+
&& chown -R runner:runner /home/runner
|
|
156
|
+
|
|
157
|
+
# Deployment additions are always present because staging writes no-op defaults
|
|
158
|
+
# when no overlay is supplied.
|
|
159
|
+
COPY overlay /etc/evident/overlay
|
|
160
|
+
# HOME is already /home/runner, so root-warmed npx/npm state must be runner-owned
|
|
161
|
+
# or the runner's first npm/pnpm call would fail with EACCES.
|
|
162
|
+
RUN /etc/evident/overlay/setup-root \
|
|
163
|
+
&& chown -R runner:runner /home/runner
|
|
178
164
|
COPY --chown=10001:10001 repo ${WORKSPACE}
|
|
179
165
|
WORKDIR ${WORKSPACE}
|
|
180
166
|
USER runner
|
|
@@ -185,36 +171,13 @@ USER runner
|
|
|
185
171
|
# `runner` (not root) so the rebuilt index is runner-owned and writable later.
|
|
186
172
|
RUN git reset --quiet
|
|
187
173
|
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
# ONE layer on purpose: pnpm hard-links node_modules into its content-addressed
|
|
192
|
-
# store (which it puts at ${WORKSPACE}/.pnpm-store, gitignored), and hard links
|
|
193
|
-
# only survive within a single layer — split in two and the image carries both
|
|
194
|
-
# copies. `--frozen-lockfile` because the committed lockfile is the whole reason
|
|
195
|
-
# this is bakeable at all: installing against anything else would bake a
|
|
196
|
-
# dependency set nobody committed.
|
|
197
|
-
#
|
|
198
|
-
# Guarded on a committed `pnpm-lock.yaml` so `microvm:repositoryPath` can point
|
|
199
|
-
# at a repository that is not a pnpm workspace at all: it is baked uninstalled
|
|
200
|
-
# instead of failing the build, and the agent installs on first use.
|
|
174
|
+
# Deployment-specific dependency installation and build work belongs here. This runs
|
|
175
|
+
# as `runner` in `${WORKSPACE}`, so everything it writes is agent-writable.
|
|
201
176
|
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
|
|
205
|
-
RUN if [ -f pnpm-lock.yaml ]; then \
|
|
206
|
-
pnpm install --frozen-lockfile \
|
|
207
|
-
&& pnpm run build --filter='./packages/*' \
|
|
208
|
-
--filter='./aws/runner-cdk' \
|
|
209
|
-
--filter='./aws/lambda-microvm-cdk' \
|
|
210
|
-
--filter='./aws/lambda-microvm-runtime'; \
|
|
211
|
-
else \
|
|
212
|
-
echo "[workspace-prep] no pnpm-lock.yaml in the baked repository — skipping the dependency install and the workspace build; the agent installs on first use."; \
|
|
213
|
-
fi
|
|
177
|
+
# Keep the overlay's install and build in one layer: pnpm hard-links `node_modules`
|
|
178
|
+
# into its content-addressed store, and hard links only survive within a single layer.
|
|
179
|
+
RUN /etc/evident/overlay/setup-workspace
|
|
214
180
|
|
|
215
|
-
# Data dir for the local dev Postgres cluster; listens on 5433 to match this
|
|
216
|
-
# repo's .envrc DSN postgres://postgres:postgres@localhost:5433/evident.
|
|
217
|
-
ENV PGDATA=/var/lib/runner-pg/data
|
|
218
181
|
# Port AWS calls the MicroVM hooks on.
|
|
219
182
|
ENV HOOKS_PORT=8080
|
|
220
183
|
# OpenCode loopback port the CLI tunnels to (matches `evident run` default).
|