@capsulos/provider-lambda 0.1.0 → 0.1.1
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/dist/index.d.ts +14 -3
- package/dist/index.js +69 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +70 -27
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type LambdaMicrovmsClientConfig } from '@aws-sdk/client-lambda-microvms';
|
|
2
|
-
import { type CapsuleHandle, type CapsuleInfo, type CapsuleSpec, type Connection, type Provider } from '@capsulos/sdk';
|
|
2
|
+
import { type CapsuleHandle, type CapsuleInfo, type CapsuleSpec, type Connection, type CreateOptions, type HeadersOptions, type Provider } from '@capsulos/sdk';
|
|
3
3
|
export interface LambdaProviderOptions {
|
|
4
4
|
region: string;
|
|
5
5
|
/** AWS account id, used to build image ARNs from template names. */
|
|
@@ -26,14 +26,25 @@ export declare class LambdaProvider implements Provider {
|
|
|
26
26
|
constructor(opts: LambdaProviderOptions);
|
|
27
27
|
/** Templates are MicrovmImages; the API wants the full ARN (a plain name fails). */
|
|
28
28
|
imageArn(template: string): string;
|
|
29
|
-
create(spec: CapsuleSpec): Promise<CapsuleHandle>;
|
|
29
|
+
create(spec: CapsuleSpec, opts?: CreateOptions): Promise<CapsuleHandle>;
|
|
30
30
|
get(id: string): Promise<CapsuleInfo | null>;
|
|
31
31
|
/** All microVMs of one template (the API lists per image). */
|
|
32
32
|
list(template?: string): Promise<CapsuleInfo[]>;
|
|
33
33
|
terminate(id: string): Promise<void>;
|
|
34
34
|
connect(id: string): Promise<Connection>;
|
|
35
|
+
/** A Connection from an endpoint already in hand (get() returns it): no extra GetMicrovm. */
|
|
36
|
+
connectionFor(id: string, endpoint: string): Connection;
|
|
35
37
|
/** Headers to reach another port of the capsule through the same endpoint. */
|
|
36
|
-
portHeaders(id: string, port: number): Promise<Record<string, string>>;
|
|
38
|
+
portHeaders(id: string, port: number, opts?: HeadersOptions): Promise<Record<string, string>>;
|
|
39
|
+
/** Forget the cached endpoint token: the next request mints a new one. */
|
|
40
|
+
invalidateToken(id: string): void;
|
|
41
|
+
private infoOf;
|
|
37
42
|
private token;
|
|
43
|
+
/**
|
|
44
|
+
* Poll until the VM runs. A SUSPENDED VM (a clientToken that recovered an
|
|
45
|
+
* idle one) counts as alive: the first request auto-resumes it. Timeout and
|
|
46
|
+
* abort throw a CapsuleError that names the VM (capsuleId) for the caller
|
|
47
|
+
* to clean up; create() does that itself.
|
|
48
|
+
*/
|
|
38
49
|
private waitForRunning;
|
|
39
50
|
}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// - endpoint tokens expire after at most 60 min: cached in memory, re-minted
|
|
9
9
|
// - other ports are addressed with the X-aws-proxy-port header
|
|
10
10
|
// - the idle policy suspends and the first request auto-resumes (~0.8 s)
|
|
11
|
-
// - clientToken is both idempotency and recovery (same token → same VM)
|
|
11
|
+
// - clientToken is both idempotency and recovery (same token → same VM);
|
|
12
|
+
// a recovered VM may be SUSPENDED, which create() accepts as alive
|
|
12
13
|
import { LambdaMicrovmsClient, RunMicrovmCommand, GetMicrovmCommand, ListMicrovmsCommand, TerminateMicrovmCommand, CreateMicrovmAuthTokenCommand, } from '@aws-sdk/client-lambda-microvms';
|
|
13
14
|
import { CapsuleError, } from '@capsulos/sdk';
|
|
14
15
|
const CAPSULOD_PORT = 8080;
|
|
@@ -46,7 +47,7 @@ export class LambdaProvider {
|
|
|
46
47
|
return template;
|
|
47
48
|
return `arn:aws:lambda:${this.opts.region}:${this.opts.accountId}:microvm-image:${template}`;
|
|
48
49
|
}
|
|
49
|
-
async create(spec) {
|
|
50
|
+
async create(spec, opts = {}) {
|
|
50
51
|
const ingress = this.opts.ingressConnectorArn ??
|
|
51
52
|
`arn:aws:lambda:${this.opts.region}:aws:network-connector:aws-network-connector:ALL_INGRESS`;
|
|
52
53
|
const res = await this.client.send(new RunMicrovmCommand({
|
|
@@ -70,21 +71,28 @@ export class LambdaProvider {
|
|
|
70
71
|
}));
|
|
71
72
|
if (!res.microvmId)
|
|
72
73
|
throw new CapsuleError('RunMicrovm returned no microvmId');
|
|
73
|
-
const
|
|
74
|
-
|
|
74
|
+
const id = res.microvmId;
|
|
75
|
+
let info;
|
|
76
|
+
try {
|
|
77
|
+
info = await this.waitForRunning(id, opts);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
// Never leave a VM running without an owner: the caller gets an error,
|
|
81
|
+
// not a capsule, so the VM goes too (the error still names it).
|
|
82
|
+
if (err instanceof CapsuleError && err.capsuleId === id) {
|
|
83
|
+
await this.terminate(id).catch(() => undefined);
|
|
84
|
+
throw new CapsuleError(`${err.message}; terminated it`, err.status, { capsuleId: id });
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
if (!info.endpoint)
|
|
89
|
+
throw new CapsuleError(`microVM ${id} is ${info.state} but has no endpoint`, undefined, { capsuleId: id });
|
|
90
|
+
return { ...info, connection: this.connectionFor(id, info.endpoint) };
|
|
75
91
|
}
|
|
76
92
|
async get(id) {
|
|
77
93
|
try {
|
|
78
94
|
const vm = await this.client.send(new GetMicrovmCommand({ microvmIdentifier: id }));
|
|
79
|
-
return
|
|
80
|
-
id,
|
|
81
|
-
provider: this.name,
|
|
82
|
-
state: toState(vm.state),
|
|
83
|
-
template: vm.imageArn ?? null,
|
|
84
|
-
createdAt: vm.startedAt ?? null,
|
|
85
|
-
terminatedAt: vm.terminatedAt ?? null,
|
|
86
|
-
stateReason: vm.stateReason ?? null,
|
|
87
|
-
};
|
|
95
|
+
return this.infoOf(id, vm);
|
|
88
96
|
}
|
|
89
97
|
catch (err) {
|
|
90
98
|
if (err instanceof Error && err.name === 'ResourceNotFoundException')
|
|
@@ -103,11 +111,13 @@ export class LambdaProvider {
|
|
|
103
111
|
for (const item of page.items ?? []) {
|
|
104
112
|
if (!item.microvmId)
|
|
105
113
|
continue;
|
|
114
|
+
// The list item carries no endpoint, terminatedAt or stateReason: get() does.
|
|
106
115
|
out.push({
|
|
107
116
|
id: item.microvmId,
|
|
108
117
|
provider: this.name,
|
|
109
118
|
state: toState(item.state),
|
|
110
119
|
template: item.imageArn ?? null,
|
|
120
|
+
templateVersion: item.imageVersion ?? null,
|
|
111
121
|
createdAt: item.startedAt ?? null,
|
|
112
122
|
});
|
|
113
123
|
}
|
|
@@ -131,19 +141,41 @@ export class LambdaProvider {
|
|
|
131
141
|
async connect(id) {
|
|
132
142
|
const vm = await this.client.send(new GetMicrovmCommand({ microvmIdentifier: id }));
|
|
133
143
|
if (!vm.endpoint)
|
|
134
|
-
throw new CapsuleError(`microVM ${id} has no endpoint yet (state ${vm.state})
|
|
144
|
+
throw new CapsuleError(`microVM ${id} has no endpoint yet (state ${vm.state})`, undefined, { capsuleId: id });
|
|
145
|
+
return this.connectionFor(id, vm.endpoint);
|
|
146
|
+
}
|
|
147
|
+
/** A Connection from an endpoint already in hand (get() returns it): no extra GetMicrovm. */
|
|
148
|
+
connectionFor(id, endpoint) {
|
|
135
149
|
return {
|
|
136
|
-
baseUrl: `https://${
|
|
137
|
-
headers: async () => ({ 'X-aws-proxy-auth': await this.token(id) }),
|
|
150
|
+
baseUrl: endpoint.startsWith('https://') ? endpoint : `https://${endpoint}`,
|
|
151
|
+
headers: async (opts) => ({ 'X-aws-proxy-auth': await this.token(id, opts?.fresh) }),
|
|
138
152
|
};
|
|
139
153
|
}
|
|
140
154
|
/** Headers to reach another port of the capsule through the same endpoint. */
|
|
141
|
-
async portHeaders(id, port) {
|
|
142
|
-
return { 'X-aws-proxy-auth': await this.token(id), 'X-aws-proxy-port': String(port) };
|
|
155
|
+
async portHeaders(id, port, opts) {
|
|
156
|
+
return { 'X-aws-proxy-auth': await this.token(id, opts?.fresh), 'X-aws-proxy-port': String(port) };
|
|
143
157
|
}
|
|
144
|
-
|
|
158
|
+
/** Forget the cached endpoint token: the next request mints a new one. */
|
|
159
|
+
invalidateToken(id) {
|
|
160
|
+
this.tokens.delete(id);
|
|
161
|
+
}
|
|
162
|
+
infoOf(id, vm) {
|
|
163
|
+
return {
|
|
164
|
+
id,
|
|
165
|
+
provider: this.name,
|
|
166
|
+
state: toState(vm.state),
|
|
167
|
+
template: vm.imageArn ?? null,
|
|
168
|
+
templateVersion: vm.imageVersion ?? null,
|
|
169
|
+
createdAt: vm.startedAt ?? null,
|
|
170
|
+
terminatedAt: vm.terminatedAt ?? null,
|
|
171
|
+
stateReason: vm.stateReason ?? null,
|
|
172
|
+
endpoint: vm.endpoint ? `https://${vm.endpoint}` : null,
|
|
173
|
+
maxDurationSeconds: vm.maximumDurationInSeconds ?? null,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async token(id, fresh = false) {
|
|
145
177
|
const cached = this.tokens.get(id);
|
|
146
|
-
if (cached && cached.expiresAt > Date.now())
|
|
178
|
+
if (cached && cached.expiresAt > Date.now() && !fresh)
|
|
147
179
|
return cached.value;
|
|
148
180
|
const ttl = Math.min(this.opts.tokenTtlMinutes ?? 60, 60);
|
|
149
181
|
const ports = new Set([CAPSULOD_PORT, ...(this.opts.allowedPorts ?? [])]);
|
|
@@ -159,18 +191,27 @@ export class LambdaProvider {
|
|
|
159
191
|
this.tokens.set(id, { value, expiresAt: Date.now() + (ttl - 5) * 60_000 });
|
|
160
192
|
return value;
|
|
161
193
|
}
|
|
162
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Poll until the VM runs. A SUSPENDED VM (a clientToken that recovered an
|
|
196
|
+
* idle one) counts as alive: the first request auto-resumes it. Timeout and
|
|
197
|
+
* abort throw a CapsuleError that names the VM (capsuleId) for the caller
|
|
198
|
+
* to clean up; create() does that itself.
|
|
199
|
+
*/
|
|
200
|
+
async waitForRunning(id, opts = {}) {
|
|
201
|
+
const timeoutMs = opts.timeoutMs ?? 120_000;
|
|
163
202
|
const t0 = Date.now();
|
|
164
203
|
for (;;) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
204
|
+
if (opts.signal?.aborted)
|
|
205
|
+
throw new CapsuleError(`creation of microVM ${id} aborted`, undefined, { capsuleId: id });
|
|
206
|
+
const vm = await this.client.send(new GetMicrovmCommand({ microvmIdentifier: id }), { abortSignal: opts.signal });
|
|
207
|
+
if (vm.state === 'RUNNING' || vm.state === 'SUSPENDED')
|
|
208
|
+
return this.infoOf(id, vm);
|
|
169
209
|
if (vm.state === 'TERMINATED' || vm.state === 'TERMINATING') {
|
|
170
|
-
throw new CapsuleError(`microVM ${id} died while starting: ${vm.stateReason ?? vm.state}
|
|
210
|
+
throw new CapsuleError(`microVM ${id} died while starting: ${vm.stateReason ?? vm.state}`, undefined, { capsuleId: id });
|
|
211
|
+
}
|
|
212
|
+
if (Date.now() - t0 > timeoutMs) {
|
|
213
|
+
throw new CapsuleError(`microVM ${id} did not run within ${timeoutMs}ms (state ${vm.state})`, undefined, { capsuleId: id });
|
|
171
214
|
}
|
|
172
|
-
if (Date.now() - t0 > timeoutMs)
|
|
173
|
-
throw new CapsuleError(`timeout waiting for microVM ${id} to run`);
|
|
174
215
|
await new Promise((r) => setTimeout(r, 400));
|
|
175
216
|
}
|
|
176
217
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,6EAA6E;AAC7E,yEAAyE;AACzE,EAAE;AACF,8DAA8D;AAC9D,gFAAgF;AAChF,8EAA8E;AAC9E,+EAA+E;AAC/E,iEAAiE;AACjE,2EAA2E;AAC3E,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capsulos/provider-lambda",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "capsulos provider for AWS Lambda MicroVMs: capsules on your own AWS account.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@aws-sdk/client-lambda-microvms": "^3.1158.0",
|
|
47
|
-
"@capsulos/sdk": "0.1.
|
|
47
|
+
"@capsulos/sdk": "0.1.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^22.20.1",
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
// - endpoint tokens expire after at most 60 min: cached in memory, re-minted
|
|
9
9
|
// - other ports are addressed with the X-aws-proxy-port header
|
|
10
10
|
// - the idle policy suspends and the first request auto-resumes (~0.8 s)
|
|
11
|
-
// - clientToken is both idempotency and recovery (same token → same VM)
|
|
11
|
+
// - clientToken is both idempotency and recovery (same token → same VM);
|
|
12
|
+
// a recovered VM may be SUSPENDED, which create() accepts as alive
|
|
12
13
|
import {
|
|
13
14
|
LambdaMicrovmsClient,
|
|
14
15
|
RunMicrovmCommand,
|
|
@@ -25,6 +26,8 @@ import {
|
|
|
25
26
|
type CapsuleSpec,
|
|
26
27
|
type CapsuleState,
|
|
27
28
|
type Connection,
|
|
29
|
+
type CreateOptions,
|
|
30
|
+
type HeadersOptions,
|
|
28
31
|
type Provider,
|
|
29
32
|
} from '@capsulos/sdk';
|
|
30
33
|
|
|
@@ -84,7 +87,7 @@ export class LambdaProvider implements Provider {
|
|
|
84
87
|
return `arn:aws:lambda:${this.opts.region}:${this.opts.accountId}:microvm-image:${template}`;
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
async create(spec: CapsuleSpec): Promise<CapsuleHandle> {
|
|
90
|
+
async create(spec: CapsuleSpec, opts: CreateOptions = {}): Promise<CapsuleHandle> {
|
|
88
91
|
const ingress =
|
|
89
92
|
this.opts.ingressConnectorArn ??
|
|
90
93
|
`arn:aws:lambda:${this.opts.region}:aws:network-connector:aws-network-connector:ALL_INGRESS`;
|
|
@@ -110,22 +113,27 @@ export class LambdaProvider implements Provider {
|
|
|
110
113
|
}),
|
|
111
114
|
);
|
|
112
115
|
if (!res.microvmId) throw new CapsuleError('RunMicrovm returned no microvmId');
|
|
113
|
-
const
|
|
114
|
-
|
|
116
|
+
const id = res.microvmId;
|
|
117
|
+
let info: CapsuleInfo;
|
|
118
|
+
try {
|
|
119
|
+
info = await this.waitForRunning(id, opts);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
// Never leave a VM running without an owner: the caller gets an error,
|
|
122
|
+
// not a capsule, so the VM goes too (the error still names it).
|
|
123
|
+
if (err instanceof CapsuleError && err.capsuleId === id) {
|
|
124
|
+
await this.terminate(id).catch(() => undefined);
|
|
125
|
+
throw new CapsuleError(`${err.message}; terminated it`, err.status, { capsuleId: id });
|
|
126
|
+
}
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
if (!info.endpoint) throw new CapsuleError(`microVM ${id} is ${info.state} but has no endpoint`, undefined, { capsuleId: id });
|
|
130
|
+
return { ...info, connection: this.connectionFor(id, info.endpoint) };
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
async get(id: string): Promise<CapsuleInfo | null> {
|
|
118
134
|
try {
|
|
119
135
|
const vm = await this.client.send(new GetMicrovmCommand({ microvmIdentifier: id }));
|
|
120
|
-
return
|
|
121
|
-
id,
|
|
122
|
-
provider: this.name,
|
|
123
|
-
state: toState(vm.state),
|
|
124
|
-
template: vm.imageArn ?? null,
|
|
125
|
-
createdAt: vm.startedAt ?? null,
|
|
126
|
-
terminatedAt: vm.terminatedAt ?? null,
|
|
127
|
-
stateReason: vm.stateReason ?? null,
|
|
128
|
-
};
|
|
136
|
+
return this.infoOf(id, vm);
|
|
129
137
|
} catch (err) {
|
|
130
138
|
if (err instanceof Error && err.name === 'ResourceNotFoundException') return null;
|
|
131
139
|
throw err;
|
|
@@ -143,11 +151,13 @@ export class LambdaProvider implements Provider {
|
|
|
143
151
|
);
|
|
144
152
|
for (const item of page.items ?? []) {
|
|
145
153
|
if (!item.microvmId) continue;
|
|
154
|
+
// The list item carries no endpoint, terminatedAt or stateReason: get() does.
|
|
146
155
|
out.push({
|
|
147
156
|
id: item.microvmId,
|
|
148
157
|
provider: this.name,
|
|
149
158
|
state: toState(item.state),
|
|
150
159
|
template: item.imageArn ?? null,
|
|
160
|
+
templateVersion: item.imageVersion ?? null,
|
|
151
161
|
createdAt: item.startedAt ?? null,
|
|
152
162
|
});
|
|
153
163
|
}
|
|
@@ -169,21 +179,46 @@ export class LambdaProvider implements Provider {
|
|
|
169
179
|
|
|
170
180
|
async connect(id: string): Promise<Connection> {
|
|
171
181
|
const vm = await this.client.send(new GetMicrovmCommand({ microvmIdentifier: id }));
|
|
172
|
-
if (!vm.endpoint) throw new CapsuleError(`microVM ${id} has no endpoint yet (state ${vm.state})
|
|
182
|
+
if (!vm.endpoint) throw new CapsuleError(`microVM ${id} has no endpoint yet (state ${vm.state})`, undefined, { capsuleId: id });
|
|
183
|
+
return this.connectionFor(id, vm.endpoint);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A Connection from an endpoint already in hand (get() returns it): no extra GetMicrovm. */
|
|
187
|
+
connectionFor(id: string, endpoint: string): Connection {
|
|
173
188
|
return {
|
|
174
|
-
baseUrl: `https://${
|
|
175
|
-
headers: async () => ({ 'X-aws-proxy-auth': await this.token(id) }),
|
|
189
|
+
baseUrl: endpoint.startsWith('https://') ? endpoint : `https://${endpoint}`,
|
|
190
|
+
headers: async (opts?: HeadersOptions) => ({ 'X-aws-proxy-auth': await this.token(id, opts?.fresh) }),
|
|
176
191
|
};
|
|
177
192
|
}
|
|
178
193
|
|
|
179
194
|
/** Headers to reach another port of the capsule through the same endpoint. */
|
|
180
|
-
async portHeaders(id: string, port: number): Promise<Record<string, string>> {
|
|
181
|
-
return { 'X-aws-proxy-auth': await this.token(id), 'X-aws-proxy-port': String(port) };
|
|
195
|
+
async portHeaders(id: string, port: number, opts?: HeadersOptions): Promise<Record<string, string>> {
|
|
196
|
+
return { 'X-aws-proxy-auth': await this.token(id, opts?.fresh), 'X-aws-proxy-port': String(port) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Forget the cached endpoint token: the next request mints a new one. */
|
|
200
|
+
invalidateToken(id: string): void {
|
|
201
|
+
this.tokens.delete(id);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private infoOf(id: string, vm: { state?: string; imageArn?: string; imageVersion?: string; startedAt?: Date; terminatedAt?: Date; stateReason?: string; endpoint?: string; maximumDurationInSeconds?: number }): CapsuleInfo {
|
|
205
|
+
return {
|
|
206
|
+
id,
|
|
207
|
+
provider: this.name,
|
|
208
|
+
state: toState(vm.state),
|
|
209
|
+
template: vm.imageArn ?? null,
|
|
210
|
+
templateVersion: vm.imageVersion ?? null,
|
|
211
|
+
createdAt: vm.startedAt ?? null,
|
|
212
|
+
terminatedAt: vm.terminatedAt ?? null,
|
|
213
|
+
stateReason: vm.stateReason ?? null,
|
|
214
|
+
endpoint: vm.endpoint ? `https://${vm.endpoint}` : null,
|
|
215
|
+
maxDurationSeconds: vm.maximumDurationInSeconds ?? null,
|
|
216
|
+
};
|
|
182
217
|
}
|
|
183
218
|
|
|
184
|
-
private async token(id: string): Promise<string> {
|
|
219
|
+
private async token(id: string, fresh = false): Promise<string> {
|
|
185
220
|
const cached = this.tokens.get(id);
|
|
186
|
-
if (cached && cached.expiresAt > Date.now()) return cached.value;
|
|
221
|
+
if (cached && cached.expiresAt > Date.now() && !fresh) return cached.value;
|
|
187
222
|
const ttl = Math.min(this.opts.tokenTtlMinutes ?? 60, 60);
|
|
188
223
|
const ports = new Set([CAPSULOD_PORT, ...(this.opts.allowedPorts ?? [])]);
|
|
189
224
|
const res = await this.client.send(
|
|
@@ -200,17 +235,25 @@ export class LambdaProvider implements Provider {
|
|
|
200
235
|
return value;
|
|
201
236
|
}
|
|
202
237
|
|
|
203
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Poll until the VM runs. A SUSPENDED VM (a clientToken that recovered an
|
|
240
|
+
* idle one) counts as alive: the first request auto-resumes it. Timeout and
|
|
241
|
+
* abort throw a CapsuleError that names the VM (capsuleId) for the caller
|
|
242
|
+
* to clean up; create() does that itself.
|
|
243
|
+
*/
|
|
244
|
+
private async waitForRunning(id: string, opts: CreateOptions = {}): Promise<CapsuleInfo> {
|
|
245
|
+
const timeoutMs = opts.timeoutMs ?? 120_000;
|
|
204
246
|
const t0 = Date.now();
|
|
205
247
|
for (;;) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
248
|
+
if (opts.signal?.aborted) throw new CapsuleError(`creation of microVM ${id} aborted`, undefined, { capsuleId: id });
|
|
249
|
+
const vm = await this.client.send(new GetMicrovmCommand({ microvmIdentifier: id }), { abortSignal: opts.signal });
|
|
250
|
+
if (vm.state === 'RUNNING' || vm.state === 'SUSPENDED') return this.infoOf(id, vm);
|
|
210
251
|
if (vm.state === 'TERMINATED' || vm.state === 'TERMINATING') {
|
|
211
|
-
throw new CapsuleError(`microVM ${id} died while starting: ${vm.stateReason ?? vm.state}
|
|
252
|
+
throw new CapsuleError(`microVM ${id} died while starting: ${vm.stateReason ?? vm.state}`, undefined, { capsuleId: id });
|
|
253
|
+
}
|
|
254
|
+
if (Date.now() - t0 > timeoutMs) {
|
|
255
|
+
throw new CapsuleError(`microVM ${id} did not run within ${timeoutMs}ms (state ${vm.state})`, undefined, { capsuleId: id });
|
|
212
256
|
}
|
|
213
|
-
if (Date.now() - t0 > timeoutMs) throw new CapsuleError(`timeout waiting for microVM ${id} to run`);
|
|
214
257
|
await new Promise((r) => setTimeout(r, 400));
|
|
215
258
|
}
|
|
216
259
|
}
|