@evident-ai/runner-cdk 3.4.1-dev.31006db → 3.4.1-dev.38181ff
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 +23 -9
- 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 +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 +31 -49
- 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
|
@@ -106,17 +106,36 @@ function stageRepository(repositoryPath, destination, originUrl) {
|
|
|
106
106
|
console.log(`[stage] ${repositoryPath} has no pnpm-lock.yaml — the image build will skip dependency installation`);
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
function stageScriptDirectory(source, destination) {
|
|
110
|
+
(0, node_fs_1.mkdirSync)(destination);
|
|
111
|
+
for (const entry of (0, node_fs_1.readdirSync)(source, { withFileTypes: true })) {
|
|
112
|
+
const sourcePath = path.join(source, entry.name);
|
|
113
|
+
const staged = path.join(destination, entry.name);
|
|
114
|
+
if (entry.isDirectory()) {
|
|
115
|
+
stageScriptDirectory(sourcePath, staged);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
(0, node_fs_1.copyFileSync)(sourcePath, staged);
|
|
119
|
+
// Set modes here rather than inheriting them from the template, so they stay
|
|
120
|
+
// correct after an npm pack, zip, CI-cache or hand-copy round trip. The image
|
|
121
|
+
// executes extensionless scripts and sources `.sh` files.
|
|
122
|
+
(0, node_fs_1.chmodSync)(staged, entry.name.endsWith('.sh') ? 0o644 : 0o755);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
109
125
|
/**
|
|
110
126
|
* 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`
|
|
127
|
+
* per-phase hook scripts, the deployment overlay and the repository — and
|
|
128
|
+
* returns its path. The result is what `EvidentMicrovmConstruct`'s `imageSource`
|
|
129
|
+
* takes.
|
|
113
130
|
*
|
|
114
|
-
* The
|
|
115
|
-
*
|
|
116
|
-
*
|
|
131
|
+
* The Dockerfile, hook server and phase hooks come from this package's
|
|
132
|
+
* published `dist/`, so a consumer needs no checkout of the source repository and no
|
|
133
|
+
* copy of those files (#1528). A caller may supply an overlay; the default is
|
|
134
|
+
* deterministic no-op scripts. `repositoryPath` is the only required caller
|
|
135
|
+
* input.
|
|
117
136
|
*/
|
|
118
137
|
function stageMicrovmImageContext(options) {
|
|
119
|
-
const { originUrl, templateDir = TEMPLATE_DIR } = options;
|
|
138
|
+
const { originUrl, overlayDir, templateDir = TEMPLATE_DIR } = options;
|
|
120
139
|
// `file://` and the clone below only mean anything against absolute paths,
|
|
121
140
|
// and a caller may reasonably pass either.
|
|
122
141
|
const repositoryPath = path.resolve(options.repositoryPath);
|
|
@@ -132,16 +151,27 @@ function stageMicrovmImageContext(options) {
|
|
|
132
151
|
(0, node_fs_1.copyFileSync)(path.join(templateDir, 'hook-server.js'), path.join(destination, 'hook-server.js'));
|
|
133
152
|
const hooksSource = path.join(templateDir, 'hooks');
|
|
134
153
|
const hooksStage = path.join(destination, 'hooks');
|
|
135
|
-
(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
(0, node_fs_1.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
154
|
+
stageScriptDirectory(hooksSource, hooksStage);
|
|
155
|
+
const overlayStage = path.join(destination, 'overlay');
|
|
156
|
+
if (overlayDir === undefined) {
|
|
157
|
+
(0, node_fs_1.mkdirSync)(overlayStage);
|
|
158
|
+
for (const name of ['setup-root', 'setup-workspace']) {
|
|
159
|
+
const staged = path.join(overlayStage, name);
|
|
160
|
+
(0, node_fs_1.writeFileSync)(staged, '#!/usr/bin/env bash\n# No overlay supplied.\n', { mode: 0o755 });
|
|
161
|
+
(0, node_fs_1.chmodSync)(staged, 0o755);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const resolvedOverlayDir = path.resolve(overlayDir);
|
|
166
|
+
if (!(0, node_fs_1.existsSync)(resolvedOverlayDir)) {
|
|
167
|
+
throw new Error(`overlay directory does not exist: ${resolvedOverlayDir}`);
|
|
168
|
+
}
|
|
169
|
+
for (const required of ['setup-root', 'setup-workspace']) {
|
|
170
|
+
if (!(0, node_fs_1.existsSync)(path.join(resolvedOverlayDir, required))) {
|
|
171
|
+
throw new Error(`overlay directory ${resolvedOverlayDir} is missing required script ${required}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
stageScriptDirectory(resolvedOverlayDir, overlayStage);
|
|
145
175
|
}
|
|
146
176
|
stageRepository(repositoryPath, path.join(destination, 'repo'), originUrl);
|
|
147
177
|
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
|
|
5
|
+
# `overlay/setup-workspace` are the extension point for deployment-specific
|
|
6
|
+
# packages 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 \
|
|
@@ -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,8 +171,8 @@ 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
|
-
# Dependencies
|
|
189
|
-
#
|
|
174
|
+
# Dependencies for the baked workspace. Install as the runner user so
|
|
175
|
+
# node_modules is writable by the agent's own later installs.
|
|
190
176
|
#
|
|
191
177
|
# ONE layer on purpose: pnpm hard-links node_modules into its content-addressed
|
|
192
178
|
# store (which it puts at ${WORKSPACE}/.pnpm-store, gitignored), and hard links
|
|
@@ -199,22 +185,18 @@ RUN git reset --quiet
|
|
|
199
185
|
# at a repository that is not a pnpm workspace at all: it is baked uninstalled
|
|
200
186
|
# instead of failing the build, and the agent installs on first use.
|
|
201
187
|
#
|
|
202
|
-
#
|
|
203
|
-
#
|
|
204
|
-
# database stay per-VM (see entrypoint.sh's pre-warm in evident-runner).
|
|
188
|
+
# Anything beyond dependency installation is deployment-specific and belongs in
|
|
189
|
+
# `overlay/setup-workspace`.
|
|
205
190
|
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'; \
|
|
191
|
+
pnpm install --frozen-lockfile; \
|
|
211
192
|
else \
|
|
212
|
-
echo "[workspace-prep] no pnpm-lock.yaml in the baked repository — skipping the dependency install
|
|
193
|
+
echo "[workspace-prep] no pnpm-lock.yaml in the baked repository — skipping the dependency install; the agent installs on first use."; \
|
|
213
194
|
fi
|
|
214
195
|
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
|
|
196
|
+
# Runs as `runner` in `${WORKSPACE}`, after the install, so a project's build
|
|
197
|
+
# output is writable by the agent.
|
|
198
|
+
RUN /etc/evident/overlay/setup-workspace
|
|
199
|
+
|
|
218
200
|
# Port AWS calls the MicroVM hooks on.
|
|
219
201
|
ENV HOOKS_PORT=8080
|
|
220
202
|
# OpenCode loopback port the CLI tunnels to (matches `evident run` default).
|