@capsulos/sdk 0.1.0 → 0.1.2
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/capsule.d.ts +4 -0
- package/dist/capsule.js +5 -3
- package/dist/capsule.js.map +1 -1
- package/dist/client.js +6 -2
- package/dist/client.js.map +1 -1
- package/dist/ports.js +1 -1
- package/dist/ports.js.map +1 -1
- package/dist/types.d.ts +30 -4
- package/dist/types.js +5 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/src/capsule.ts +8 -3
- package/src/client.test.ts +30 -0
- package/src/client.ts +6 -2
- package/src/e2e.test.ts +2 -1
- package/src/ports.ts +1 -1
- package/src/types.ts +32 -3
package/dist/capsule.d.ts
CHANGED
|
@@ -5,6 +5,10 @@ import { type CapsuleSpec, type ExecOptions, type ExecResult, type FileEntry, ty
|
|
|
5
5
|
export interface CapsuleOptions extends ClientOptions {
|
|
6
6
|
/** How long to wait for the daemon to answer /health after creation. */
|
|
7
7
|
readyTimeoutMs?: number;
|
|
8
|
+
/** How long the provider may take to run the capsule (default 120 s); on timeout it is terminated. */
|
|
9
|
+
createTimeoutMs?: number;
|
|
10
|
+
/** Abort creation (the provider terminates what it started) or the wait for the daemon. */
|
|
11
|
+
signal?: AbortSignal;
|
|
8
12
|
}
|
|
9
13
|
export declare class Capsule {
|
|
10
14
|
readonly id: string;
|
package/dist/capsule.js
CHANGED
|
@@ -25,9 +25,9 @@ export class Capsule {
|
|
|
25
25
|
}
|
|
26
26
|
/** Throw a capsule: create it with the provider and wait until it is ready. */
|
|
27
27
|
static async create(provider, spec, opts = {}) {
|
|
28
|
-
const handle = await provider.create(spec);
|
|
28
|
+
const handle = await provider.create(spec, { timeoutMs: opts.createTimeoutMs, signal: opts.signal });
|
|
29
29
|
const client = new DaemonClient(handle.connection, opts);
|
|
30
|
-
const health = await waitReady(client, opts.readyTimeoutMs ?? 120_000);
|
|
30
|
+
const health = await waitReady(client, opts.readyTimeoutMs ?? 120_000, opts.signal);
|
|
31
31
|
// Providers deliver the mount table through their boot hook. When that did
|
|
32
32
|
// not happen (no hook, or the hook came without its payload), hand it over
|
|
33
33
|
// directly. Either way, a mount that failed is an error here, not a
|
|
@@ -105,10 +105,12 @@ export class Files {
|
|
|
105
105
|
return this.client.remove(path);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
-
async function waitReady(client, timeoutMs) {
|
|
108
|
+
async function waitReady(client, timeoutMs, signal) {
|
|
109
109
|
const t0 = Date.now();
|
|
110
110
|
let lastErr;
|
|
111
111
|
while (Date.now() - t0 < timeoutMs) {
|
|
112
|
+
if (signal?.aborted)
|
|
113
|
+
throw new CapsuleError('wait for the capsule daemon aborted');
|
|
112
114
|
try {
|
|
113
115
|
const h = await client.health();
|
|
114
116
|
if (h.ok)
|
package/dist/capsule.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capsule.js","sourceRoot":"","sources":["../src/capsule.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,uEAAuE;AACvE,0BAA0B;AAC1B,OAAO,EAAE,YAAY,EAAsB,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EACL,YAAY,GAQb,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"capsule.js","sourceRoot":"","sources":["../src/capsule.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -10,13 +10,17 @@ export class DaemonClient {
|
|
|
10
10
|
this.fetchImpl = opts.fetch ?? ((...args) => fetch(...args));
|
|
11
11
|
this.requestTimeoutMs = opts.requestTimeoutMs ?? 30_000;
|
|
12
12
|
}
|
|
13
|
-
async request(path, init = {}) {
|
|
14
|
-
const headers = { ...(await this.connection.headers()), ...init.headers };
|
|
13
|
+
async request(path, init = {}, fresh = false) {
|
|
14
|
+
const headers = { ...(await this.connection.headers({ fresh })), ...init.headers };
|
|
15
15
|
const res = await this.fetchImpl(`${this.connection.baseUrl}${path}`, {
|
|
16
16
|
...init,
|
|
17
17
|
headers,
|
|
18
18
|
signal: AbortSignal.timeout(init.timeoutMs ?? this.requestTimeoutMs),
|
|
19
19
|
});
|
|
20
|
+
// A rejected credential (expired or revoked endpoint token) gets one retry with a fresh one.
|
|
21
|
+
if ((res.status === 401 || res.status === 403) && !fresh) {
|
|
22
|
+
return this.request(path, init, true);
|
|
23
|
+
}
|
|
20
24
|
if (!res.ok) {
|
|
21
25
|
let detail = '';
|
|
22
26
|
try {
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,gFAAgF;AAChF,OAAO,EACL,YAAY,GASb,MAAM,YAAY,CAAC;AAOpB,MAAM,OAAO,YAAY;IAKZ;IAJM,SAAS,CAAe;IACxB,gBAAgB,CAAS;IAE1C,YACW,UAAsB,EAC/B,OAAsB,EAAE;QADf,eAAU,GAAV,UAAU,CAAY;QAG/B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;QAC7D,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,gBAAgB,IAAI,MAAM,CAAC;IAC1D,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,OAA6C,EAAE;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/ports.js
CHANGED
|
@@ -11,7 +11,7 @@ export class Ports {
|
|
|
11
11
|
/** A Connection whose baseUrl is the proxied port: build clients, WebSockets, fetches on it. */
|
|
12
12
|
connection(port) {
|
|
13
13
|
checkPort(port);
|
|
14
|
-
return { baseUrl: `${this.conn.baseUrl}/proxy/${port}`, headers: () => this.conn.headers() };
|
|
14
|
+
return { baseUrl: `${this.conn.baseUrl}/proxy/${port}`, headers: (opts) => this.conn.headers(opts) };
|
|
15
15
|
}
|
|
16
16
|
/** `fetch()` against the server on `port` inside the capsule, with the capsule's auth headers attached. */
|
|
17
17
|
async fetch(port, path = '/', init = {}) {
|
package/dist/ports.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAMA,MAAM,OAAO,KAAK;IACa;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,2GAA2G;IAC3G,GAAG,CAAC,IAAY,EAAE,IAAI,GAAG,GAAG;QAC1B,SAAS,CAAC,IAAI,CAAC,CAAC;QAChB,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,UAAU,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;IACzF,CAAC;IAED,gGAAgG;IAChG,UAAU,CAAC,IAAY;QACrB,SAAS,CAAC,IAAI,CAAC,CAAC;QAChB,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,UAAU,IAAI,EAAE,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAMA,MAAM,OAAO,KAAK;IACa;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,2GAA2G;IAC3G,GAAG,CAAC,IAAY,EAAE,IAAI,GAAG,GAAG;QAC1B,SAAS,CAAC,IAAI,CAAC,CAAC;QAChB,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,UAAU,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;IACzF,CAAC;IAED,gGAAgG;IAChG,UAAU,CAAC,IAAY;QACrB,SAAS,CAAC,IAAI,CAAC,CAAC;QAChB,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,UAAU,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;IACvG,CAAC;IAED,2GAA2G;IAC3G,KAAK,CAAC,KAAK,CAAC,IAAY,EAAE,IAAI,GAAG,GAAG,EAAE,OAAoB,EAAE;QAC1D,MAAM,OAAO,GAAG,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,GAAI,IAAI,CAAC,OAA8C,EAAE,CAAC;QAC5G,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAChG,CAAC;CACF;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,KAAK;QAAE,MAAM,IAAI,UAAU,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;AACrG,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** Where a capsule lives. Implemented by @capsulos/provider-* packages. */
|
|
2
2
|
export interface Provider {
|
|
3
3
|
readonly name: string;
|
|
4
|
-
|
|
4
|
+
/** Create a capsule and wait until it runs (see CreateOptions for the wait). */
|
|
5
|
+
create(spec: CapsuleSpec, opts?: CreateOptions): Promise<CapsuleHandle>;
|
|
5
6
|
get(id: string): Promise<CapsuleInfo | null>;
|
|
6
7
|
/** Capsules this provider knows about, optionally only those of one template. */
|
|
7
8
|
list(template?: string): Promise<CapsuleInfo[]>;
|
|
@@ -9,11 +10,26 @@ export interface Provider {
|
|
|
9
10
|
/** How to reach the daemon of an existing capsule (endpoint + auth headers). */
|
|
10
11
|
connect(id: string): Promise<Connection>;
|
|
11
12
|
}
|
|
13
|
+
export interface CreateOptions {
|
|
14
|
+
/**
|
|
15
|
+
* How long to wait for the capsule to run before giving up (provider
|
|
16
|
+
* default: 120 s). On timeout the provider terminates what it started, so
|
|
17
|
+
* nothing is left running without an owner, and throws a CapsuleError
|
|
18
|
+
* whose `capsuleId` names it.
|
|
19
|
+
*/
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** Abort the wait (same clean-up as a timeout). */
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
}
|
|
24
|
+
export interface HeadersOptions {
|
|
25
|
+
/** Discard any cached credential and mint a new one (after a 401/403). */
|
|
26
|
+
fresh?: boolean;
|
|
27
|
+
}
|
|
12
28
|
export interface Connection {
|
|
13
29
|
/** Base URL of capsulod, e.g. https://abc.microvm.lambda-url.us-east-1.on.aws */
|
|
14
30
|
baseUrl: string;
|
|
15
|
-
/** Headers every request must carry (e.g. X-aws-proxy-auth).
|
|
16
|
-
headers(): Promise<Record<string, string>>;
|
|
31
|
+
/** Headers every request must carry (e.g. X-aws-proxy-auth). Cached by the provider; `fresh` re-mints. */
|
|
32
|
+
headers(opts?: HeadersOptions): Promise<Record<string, string>>;
|
|
17
33
|
}
|
|
18
34
|
export type CapsuleState = 'pending' | 'running' | 'suspended' | 'terminated' | 'unknown';
|
|
19
35
|
export interface CapsuleInfo {
|
|
@@ -26,6 +42,12 @@ export interface CapsuleInfo {
|
|
|
26
42
|
terminatedAt?: Date | null;
|
|
27
43
|
/** The provider's reason for the current state (e.g. why it died), when it gives one. */
|
|
28
44
|
stateReason?: string | null;
|
|
45
|
+
/** Where capsulod answers (the Connection's baseUrl), when the provider knows it without another call. */
|
|
46
|
+
endpoint?: string | null;
|
|
47
|
+
/** Version of the template the capsule runs (a MicrovmImage version on Lambda). */
|
|
48
|
+
templateVersion?: string | null;
|
|
49
|
+
/** The hard wall-clock limit it was created with. */
|
|
50
|
+
maxDurationSeconds?: number | null;
|
|
29
51
|
}
|
|
30
52
|
export interface CapsuleHandle extends CapsuleInfo {
|
|
31
53
|
connection: Connection;
|
|
@@ -138,5 +160,9 @@ export interface FileEntry {
|
|
|
138
160
|
}
|
|
139
161
|
export declare class CapsuleError extends Error {
|
|
140
162
|
readonly status?: number | undefined;
|
|
141
|
-
|
|
163
|
+
/** The capsule the error is about, when there is one (e.g. the one a create() timeout terminated). */
|
|
164
|
+
readonly capsuleId?: string;
|
|
165
|
+
constructor(message: string, status?: number | undefined, opts?: {
|
|
166
|
+
capsuleId?: string;
|
|
167
|
+
});
|
|
142
168
|
}
|
package/dist/types.js
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
// Drivers — how a primitive is implemented (fs: local, tmpfs, overlay, s3)
|
|
5
5
|
export class CapsuleError extends Error {
|
|
6
6
|
status;
|
|
7
|
-
|
|
7
|
+
/** The capsule the error is about, when there is one (e.g. the one a create() timeout terminated). */
|
|
8
|
+
capsuleId;
|
|
9
|
+
constructor(message, status, opts = {}) {
|
|
8
10
|
super(message);
|
|
9
11
|
this.status = status;
|
|
10
12
|
this.name = 'CapsuleError';
|
|
13
|
+
if (opts.capsuleId)
|
|
14
|
+
this.capsuleId = opts.capsuleId;
|
|
11
15
|
}
|
|
12
16
|
}
|
|
13
17
|
//# sourceMappingURL=types.js.map
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,0EAA0E;AAC1E,kEAAkE;AAClE,gFAAgF;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,0EAA0E;AAC1E,kEAAkE;AAClE,gFAAgF;AAwKhF,MAAM,OAAO,YAAa,SAAQ,KAAK;IAM1B;IALX,sGAAsG;IAC7F,SAAS,CAAU;IAE5B,YACE,OAAe,EACN,MAAe,EACxB,OAA+B,EAAE;QAEjC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHN,WAAM,GAAN,MAAM,CAAS;QAIxB,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,IAAI,CAAC,SAAS;YAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IACtD,CAAC;CACF"}
|
package/package.json
CHANGED
package/src/capsule.ts
CHANGED
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
export interface CapsuleOptions extends ClientOptions {
|
|
19
19
|
/** How long to wait for the daemon to answer /health after creation. */
|
|
20
20
|
readyTimeoutMs?: number;
|
|
21
|
+
/** How long the provider may take to run the capsule (default 120 s); on timeout it is terminated. */
|
|
22
|
+
createTimeoutMs?: number;
|
|
23
|
+
/** Abort creation (the provider terminates what it started) or the wait for the daemon. */
|
|
24
|
+
signal?: AbortSignal;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export class Capsule {
|
|
@@ -40,9 +44,9 @@ export class Capsule {
|
|
|
40
44
|
|
|
41
45
|
/** Throw a capsule: create it with the provider and wait until it is ready. */
|
|
42
46
|
static async create(provider: Provider, spec: CapsuleSpec, opts: CapsuleOptions = {}): Promise<Capsule> {
|
|
43
|
-
const handle = await provider.create(spec);
|
|
47
|
+
const handle = await provider.create(spec, { timeoutMs: opts.createTimeoutMs, signal: opts.signal });
|
|
44
48
|
const client = new DaemonClient(handle.connection, opts);
|
|
45
|
-
const health = await waitReady(client, opts.readyTimeoutMs ?? 120_000);
|
|
49
|
+
const health = await waitReady(client, opts.readyTimeoutMs ?? 120_000, opts.signal);
|
|
46
50
|
// Providers deliver the mount table through their boot hook. When that did
|
|
47
51
|
// not happen (no hook, or the hook came without its payload), hand it over
|
|
48
52
|
// directly. Either way, a mount that failed is an error here, not a
|
|
@@ -118,10 +122,11 @@ export class Files {
|
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
async function waitReady(client: DaemonClient, timeoutMs: number): Promise<Health> {
|
|
125
|
+
async function waitReady(client: DaemonClient, timeoutMs: number, signal?: AbortSignal): Promise<Health> {
|
|
122
126
|
const t0 = Date.now();
|
|
123
127
|
let lastErr: unknown;
|
|
124
128
|
while (Date.now() - t0 < timeoutMs) {
|
|
129
|
+
if (signal?.aborted) throw new CapsuleError('wait for the capsule daemon aborted');
|
|
125
130
|
try {
|
|
126
131
|
const h = await client.health();
|
|
127
132
|
if (h.ok) return h;
|
package/src/client.test.ts
CHANGED
|
@@ -47,3 +47,33 @@ describe('DaemonClient.exec', () => {
|
|
|
47
47
|
await expect(client.readText('/x')).rejects.toThrow(/HTTP 404: nope/);
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
describe('DaemonClient credentials', () => {
|
|
52
|
+
it('retries once with fresh headers when the endpoint rejects the token', async () => {
|
|
53
|
+
const minted: boolean[] = [];
|
|
54
|
+
const conn: Connection = {
|
|
55
|
+
baseUrl: 'http://capsule',
|
|
56
|
+
headers: async (opts) => {
|
|
57
|
+
minted.push(Boolean(opts?.fresh));
|
|
58
|
+
return { 'X-Token': opts?.fresh ? 'new' : 'stale' };
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
const client = new DaemonClient(conn, {
|
|
62
|
+
fetch: async (_url, init) => {
|
|
63
|
+
const token = new Headers(init?.headers).get('X-Token');
|
|
64
|
+
return token === 'new'
|
|
65
|
+
? new Response(JSON.stringify({ ok: true, version: 't', uptimeS: 1, configured: true }), { status: 200 })
|
|
66
|
+
: new Response(JSON.stringify({ error: 'forbidden' }), { status: 403 });
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const health = await client.health();
|
|
70
|
+
expect(health.ok).toBe(true);
|
|
71
|
+
expect(minted).toEqual([false, true]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('gives up after one fresh attempt', async () => {
|
|
75
|
+
const conn: Connection = { baseUrl: 'http://capsule', headers: async () => ({}) };
|
|
76
|
+
const client = new DaemonClient(conn, { fetch: async () => new Response('{"error":"no"}', { status: 403 }) });
|
|
77
|
+
await expect(client.health()).rejects.toThrow(/HTTP 403/);
|
|
78
|
+
});
|
|
79
|
+
});
|
package/src/client.ts
CHANGED
|
@@ -29,13 +29,17 @@ export class DaemonClient {
|
|
|
29
29
|
this.requestTimeoutMs = opts.requestTimeoutMs ?? 30_000;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
private async request(path: string, init: RequestInit & { timeoutMs?: number } = {}): Promise<Response> {
|
|
33
|
-
const headers = { ...(await this.connection.headers()), ...(init.headers as Record<string, string> | undefined) };
|
|
32
|
+
private async request(path: string, init: RequestInit & { timeoutMs?: number } = {}, fresh = false): Promise<Response> {
|
|
33
|
+
const headers = { ...(await this.connection.headers({ fresh })), ...(init.headers as Record<string, string> | undefined) };
|
|
34
34
|
const res = await this.fetchImpl(`${this.connection.baseUrl}${path}`, {
|
|
35
35
|
...init,
|
|
36
36
|
headers,
|
|
37
37
|
signal: AbortSignal.timeout(init.timeoutMs ?? this.requestTimeoutMs),
|
|
38
38
|
});
|
|
39
|
+
// A rejected credential (expired or revoked endpoint token) gets one retry with a fresh one.
|
|
40
|
+
if ((res.status === 401 || res.status === 403) && !fresh) {
|
|
41
|
+
return this.request(path, init, true);
|
|
42
|
+
}
|
|
39
43
|
if (!res.ok) {
|
|
40
44
|
let detail = '';
|
|
41
45
|
try {
|
package/src/e2e.test.ts
CHANGED
|
@@ -114,7 +114,8 @@ describe.skipIf(!existsSync(BIN))('capsulod e2e', () => {
|
|
|
114
114
|
|
|
115
115
|
it('explains a port nobody listens on', async () => {
|
|
116
116
|
const res = await capsule.ports.fetch(1, '/');
|
|
117
|
-
expect(res.status).toBe(
|
|
117
|
+
expect(res.status).toBe(503);
|
|
118
|
+
expect(res.headers.get('X-Capsulos-Proxy-Error')).toBe('unreachable');
|
|
118
119
|
expect((await res.json()).error).toMatch(/nothing is listening on port 1/);
|
|
119
120
|
expect(() => capsule.ports.url(0)).toThrow(RangeError);
|
|
120
121
|
});
|
package/src/ports.ts
CHANGED
|
@@ -16,7 +16,7 @@ export class Ports {
|
|
|
16
16
|
/** A Connection whose baseUrl is the proxied port: build clients, WebSockets, fetches on it. */
|
|
17
17
|
connection(port: number): Connection {
|
|
18
18
|
checkPort(port);
|
|
19
|
-
return { baseUrl: `${this.conn.baseUrl}/proxy/${port}`, headers: () => this.conn.headers() };
|
|
19
|
+
return { baseUrl: `${this.conn.baseUrl}/proxy/${port}`, headers: (opts) => this.conn.headers(opts) };
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** `fetch()` against the server on `port` inside the capsule, with the capsule's auth headers attached. */
|
package/src/types.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
/** Where a capsule lives. Implemented by @capsulos/provider-* packages. */
|
|
7
7
|
export interface Provider {
|
|
8
8
|
readonly name: string;
|
|
9
|
-
|
|
9
|
+
/** Create a capsule and wait until it runs (see CreateOptions for the wait). */
|
|
10
|
+
create(spec: CapsuleSpec, opts?: CreateOptions): Promise<CapsuleHandle>;
|
|
10
11
|
get(id: string): Promise<CapsuleInfo | null>;
|
|
11
12
|
/** Capsules this provider knows about, optionally only those of one template. */
|
|
12
13
|
list(template?: string): Promise<CapsuleInfo[]>;
|
|
@@ -15,11 +16,28 @@ export interface Provider {
|
|
|
15
16
|
connect(id: string): Promise<Connection>;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
export interface CreateOptions {
|
|
20
|
+
/**
|
|
21
|
+
* How long to wait for the capsule to run before giving up (provider
|
|
22
|
+
* default: 120 s). On timeout the provider terminates what it started, so
|
|
23
|
+
* nothing is left running without an owner, and throws a CapsuleError
|
|
24
|
+
* whose `capsuleId` names it.
|
|
25
|
+
*/
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
/** Abort the wait (same clean-up as a timeout). */
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface HeadersOptions {
|
|
32
|
+
/** Discard any cached credential and mint a new one (after a 401/403). */
|
|
33
|
+
fresh?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
export interface Connection {
|
|
19
37
|
/** Base URL of capsulod, e.g. https://abc.microvm.lambda-url.us-east-1.on.aws */
|
|
20
38
|
baseUrl: string;
|
|
21
|
-
/** Headers every request must carry (e.g. X-aws-proxy-auth).
|
|
22
|
-
headers(): Promise<Record<string, string>>;
|
|
39
|
+
/** Headers every request must carry (e.g. X-aws-proxy-auth). Cached by the provider; `fresh` re-mints. */
|
|
40
|
+
headers(opts?: HeadersOptions): Promise<Record<string, string>>;
|
|
23
41
|
}
|
|
24
42
|
|
|
25
43
|
export type CapsuleState = 'pending' | 'running' | 'suspended' | 'terminated' | 'unknown';
|
|
@@ -34,6 +52,12 @@ export interface CapsuleInfo {
|
|
|
34
52
|
terminatedAt?: Date | null;
|
|
35
53
|
/** The provider's reason for the current state (e.g. why it died), when it gives one. */
|
|
36
54
|
stateReason?: string | null;
|
|
55
|
+
/** Where capsulod answers (the Connection's baseUrl), when the provider knows it without another call. */
|
|
56
|
+
endpoint?: string | null;
|
|
57
|
+
/** Version of the template the capsule runs (a MicrovmImage version on Lambda). */
|
|
58
|
+
templateVersion?: string | null;
|
|
59
|
+
/** The hard wall-clock limit it was created with. */
|
|
60
|
+
maxDurationSeconds?: number | null;
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
export interface CapsuleHandle extends CapsuleInfo {
|
|
@@ -146,11 +170,16 @@ export interface FileEntry {
|
|
|
146
170
|
}
|
|
147
171
|
|
|
148
172
|
export class CapsuleError extends Error {
|
|
173
|
+
/** The capsule the error is about, when there is one (e.g. the one a create() timeout terminated). */
|
|
174
|
+
readonly capsuleId?: string;
|
|
175
|
+
|
|
149
176
|
constructor(
|
|
150
177
|
message: string,
|
|
151
178
|
readonly status?: number,
|
|
179
|
+
opts: { capsuleId?: string } = {},
|
|
152
180
|
) {
|
|
153
181
|
super(message);
|
|
154
182
|
this.name = 'CapsuleError';
|
|
183
|
+
if (opts.capsuleId) this.capsuleId = opts.capsuleId;
|
|
155
184
|
}
|
|
156
185
|
}
|