@capsulos/cli 0.0.0-stage → 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/LICENSE +202 -0
- package/bin/capsulos.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +175 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +29 -0
- package/dist/config.js.map +1 -0
- package/dist/forward.d.ts +10 -0
- package/dist/forward.js +89 -0
- package/dist/forward.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +10 -0
- package/dist/init.js +106 -0
- package/dist/init.js.map +1 -0
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +31 -0
- package/dist/shell.js.map +1 -0
- package/dist/template.d.ts +39 -0
- package/dist/template.js +153 -0
- package/dist/template.js.map +1 -0
- package/package.json +68 -3
- package/src/cli.ts +171 -0
- package/src/config.ts +51 -0
- package/src/forward.ts +92 -0
- package/src/index.ts +3 -0
- package/src/init.ts +125 -0
- package/src/shell.ts +29 -0
- package/src/template.test.ts +63 -0
- package/src/template.ts +190 -0
- package/README.md +0 -3
package/dist/init.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// `capsulos init`: the one-time setup in an AWS account. Idempotent.
|
|
2
|
+
// - artifact bucket capsulos-artifacts-<account>
|
|
3
|
+
// - build role CapsulosBuildRole (assumed by the image builder)
|
|
4
|
+
// - execution role CapsulosExecutionRole (assumed by every capsule)
|
|
5
|
+
// Network connectors are deliberately not created here: they are an account
|
|
6
|
+
// design decision (which VPC, which security group). Pass one with
|
|
7
|
+
// --egress-connector to record it in capsulos.json.
|
|
8
|
+
import { IAMClient, GetRoleCommand, CreateRoleCommand, PutRolePolicyCommand } from '@aws-sdk/client-iam';
|
|
9
|
+
import { S3Client, HeadBucketCommand, CreateBucketCommand } from '@aws-sdk/client-s3';
|
|
10
|
+
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
|
11
|
+
import { credentialsFor, saveConfig } from './config.js';
|
|
12
|
+
const BUILD_ROLE = 'CapsulosBuildRole';
|
|
13
|
+
const EXEC_ROLE = 'CapsulosExecutionRole';
|
|
14
|
+
const TAGS = [
|
|
15
|
+
{ Key: 'Project', Value: 'capsulos' },
|
|
16
|
+
{ Key: 'ManagedBy', Value: 'capsulos-cli' },
|
|
17
|
+
];
|
|
18
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
19
|
+
export async function init(opts) {
|
|
20
|
+
const log = opts.log ?? (() => { });
|
|
21
|
+
const credentials = credentialsFor(opts.profile);
|
|
22
|
+
const clientOpts = { region: opts.region, credentials };
|
|
23
|
+
const sts = new STSClient(clientOpts);
|
|
24
|
+
const iam = new IAMClient(clientOpts);
|
|
25
|
+
const s3 = new S3Client(clientOpts);
|
|
26
|
+
const me = await sts.send(new GetCallerIdentityCommand({}));
|
|
27
|
+
const accountId = me.Account;
|
|
28
|
+
if (!accountId)
|
|
29
|
+
throw new Error('could not resolve the AWS account id');
|
|
30
|
+
const bucket = `capsulos-artifacts-${accountId}`;
|
|
31
|
+
try {
|
|
32
|
+
await s3.send(new HeadBucketCommand({ Bucket: bucket }));
|
|
33
|
+
log(`bucket ${bucket} exists`);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
await s3.send(new CreateBucketCommand({ Bucket: bucket }));
|
|
37
|
+
log(`bucket ${bucket} created`);
|
|
38
|
+
}
|
|
39
|
+
const trust = JSON.stringify({
|
|
40
|
+
Version: '2012-10-17',
|
|
41
|
+
Statement: [
|
|
42
|
+
{
|
|
43
|
+
Effect: 'Allow',
|
|
44
|
+
Principal: { Service: 'lambda.amazonaws.com' },
|
|
45
|
+
Action: ['sts:AssumeRole', 'sts:TagSession'],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
let created = false;
|
|
50
|
+
const ensureRole = async (name, description) => {
|
|
51
|
+
try {
|
|
52
|
+
await iam.send(new GetRoleCommand({ RoleName: name }));
|
|
53
|
+
log(`role ${name} exists`);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
await iam.send(new CreateRoleCommand({ RoleName: name, AssumeRolePolicyDocument: trust, Description: description, Tags: TAGS }));
|
|
57
|
+
created = true;
|
|
58
|
+
log(`role ${name} created`);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
await ensureRole(BUILD_ROLE, 'capsulos: assumed by the Lambda MicroVMs image builder');
|
|
62
|
+
await iam.send(new PutRolePolicyCommand({
|
|
63
|
+
RoleName: BUILD_ROLE,
|
|
64
|
+
PolicyName: 'capsulos-build',
|
|
65
|
+
PolicyDocument: JSON.stringify({
|
|
66
|
+
Version: '2012-10-17',
|
|
67
|
+
Statement: [
|
|
68
|
+
{ Effect: 'Allow', Action: ['s3:GetObject'], Resource: `arn:aws:s3:::${bucket}/*` },
|
|
69
|
+
{ Effect: 'Allow', Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], Resource: 'arn:aws:logs:*:*:*' },
|
|
70
|
+
],
|
|
71
|
+
}),
|
|
72
|
+
}));
|
|
73
|
+
await ensureRole(EXEC_ROLE, 'capsulos: assumed by every capsule (S3 mounts)');
|
|
74
|
+
const buckets = opts.s3Buckets ?? [];
|
|
75
|
+
await iam.send(new PutRolePolicyCommand({
|
|
76
|
+
RoleName: EXEC_ROLE,
|
|
77
|
+
PolicyName: 'capsulos-execution',
|
|
78
|
+
PolicyDocument: JSON.stringify({
|
|
79
|
+
Version: '2012-10-17',
|
|
80
|
+
Statement: buckets.length
|
|
81
|
+
? [
|
|
82
|
+
{ Effect: 'Allow', Action: ['s3:ListBucket'], Resource: buckets.map((b) => `arn:aws:s3:::${b}`) },
|
|
83
|
+
{ Effect: 'Allow', Action: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject'], Resource: buckets.map((b) => `arn:aws:s3:::${b}/*`) },
|
|
84
|
+
]
|
|
85
|
+
: // IAM rejects an empty statement list; grant nothing explicitly.
|
|
86
|
+
[{ Effect: 'Deny', Action: ['s3:*'], Resource: '*', Sid: 'NoBucketsConfiguredYet' }],
|
|
87
|
+
}),
|
|
88
|
+
}));
|
|
89
|
+
log(buckets.length ? `execution role: S3 access to ${buckets.join(', ')}` : 'execution role: no S3 buckets (pass --s3-bucket)');
|
|
90
|
+
// IAM is eventually consistent: a capsule launched right after a policy
|
|
91
|
+
// change can still be refused for a few seconds.
|
|
92
|
+
log(created ? 'roles just created: waiting 10s for IAM to propagate' : 'policies updated: waiting 10s for IAM to propagate');
|
|
93
|
+
await sleep(10_000);
|
|
94
|
+
const cfg = {
|
|
95
|
+
region: opts.region,
|
|
96
|
+
accountId,
|
|
97
|
+
bucket,
|
|
98
|
+
buildRoleArn: `arn:aws:iam::${accountId}:role/${BUILD_ROLE}`,
|
|
99
|
+
executionRoleArn: `arn:aws:iam::${accountId}:role/${EXEC_ROLE}`,
|
|
100
|
+
...(opts.egressConnectorArn ? { egressConnectorArn: opts.egressConnectorArn } : {}),
|
|
101
|
+
};
|
|
102
|
+
const path = saveConfig(cfg);
|
|
103
|
+
log(`wrote ${path}`);
|
|
104
|
+
return cfg;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=init.js.map
|
package/dist/init.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
|
package/dist/shell.d.ts
ADDED
package/dist/shell.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// `capsulos shell <id>`: your terminal, attached to a capsule's pty. Raw mode
|
|
2
|
+
// so that every key (ctrl-c, arrows, tab) goes to the capsule; the session
|
|
3
|
+
// ends when the remote shell exits.
|
|
4
|
+
import { Capsule } from '@capsulos/sdk';
|
|
5
|
+
export async function shell(provider, id, cmd) {
|
|
6
|
+
const capsule = await Capsule.connect(provider, id);
|
|
7
|
+
const { stdin, stdout } = process;
|
|
8
|
+
const size = () => ({ cols: stdout.columns || 80, rows: stdout.rows || 24 });
|
|
9
|
+
const term = await capsule.pty.open({ cmd, ...size(), onData: (chunk) => stdout.write(chunk) });
|
|
10
|
+
const onKey = (data) => term.write(new Uint8Array(data));
|
|
11
|
+
const onResize = () => {
|
|
12
|
+
const s = size();
|
|
13
|
+
term.resize(s.cols, s.rows);
|
|
14
|
+
};
|
|
15
|
+
if (stdin.isTTY)
|
|
16
|
+
stdin.setRawMode(true);
|
|
17
|
+
stdin.resume();
|
|
18
|
+
stdin.on('data', onKey);
|
|
19
|
+
stdout.on('resize', onResize);
|
|
20
|
+
try {
|
|
21
|
+
return await term.wait();
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
stdin.off('data', onKey);
|
|
25
|
+
stdout.off('resize', onResize);
|
|
26
|
+
if (stdin.isTTY)
|
|
27
|
+
stdin.setRawMode(false);
|
|
28
|
+
stdin.pause();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2EAA2E;AAC3E,oCAAoC;AACpC,OAAO,EAAE,OAAO,EAAiB,MAAM,eAAe,CAAC;AAEvD,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,QAAkB,EAAE,EAAU,EAAE,GAAY;IACtE,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAClC,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAEhG,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IACjE,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;QACjB,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC,CAAC;IACF,IAAI,KAAK,CAAC,KAAK;QAAE,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACxC,KAAK,CAAC,MAAM,EAAE,CAAC;IACf,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACxB,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC9B,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAC3B,CAAC;YAAS,CAAC;QACT,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC/B,IAAI,KAAK,CAAC,KAAK;YAAE,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACzC,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type CapsulosConfig } from './config.js';
|
|
2
|
+
export interface TemplateBuildOptions {
|
|
3
|
+
name: string;
|
|
4
|
+
/** Directory with the Dockerfile (and anything it COPYs). */
|
|
5
|
+
context: string;
|
|
6
|
+
dockerfile?: string;
|
|
7
|
+
/** Prebuilt linux/arm64 capsulod; when omitted, built from --daemon-src with Go. */
|
|
8
|
+
capsulod?: string;
|
|
9
|
+
/** Path to the daemon's Go module (defaults to the monorepo's daemon/). */
|
|
10
|
+
daemonSrc?: string;
|
|
11
|
+
memoryMiB?: number;
|
|
12
|
+
/**
|
|
13
|
+
* OS capabilities of the capsules: 'default' (Docker's set, seccomp on) or
|
|
14
|
+
* 'all' (every capability, seccomp off: what kernel mounts — tmpfs, overlay,
|
|
15
|
+
* FUSE/S3 — need). Default 'default': opt into 'all' when the template
|
|
16
|
+
* mounts things, knowing untrusted code inside gets a less hardened guest.
|
|
17
|
+
*/
|
|
18
|
+
capabilities?: Capabilities;
|
|
19
|
+
/** Tags on the MicrovmImage (cost allocation); `project: capsulos` unless you say otherwise. */
|
|
20
|
+
tags?: Record<string, string>;
|
|
21
|
+
profile?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
log?: (msg: string) => void;
|
|
24
|
+
}
|
|
25
|
+
export type Capabilities = 'default' | 'all';
|
|
26
|
+
export declare function imageArn(cfg: CapsulosConfig, name: string): string;
|
|
27
|
+
/** Resolve or cross-compile capsulod for linux/arm64 (the service runs Graviton). */
|
|
28
|
+
export declare function resolveCapsulod(opts: Pick<TemplateBuildOptions, 'capsulod' | 'daemonSrc' | 'log'>): string;
|
|
29
|
+
/**
|
|
30
|
+
* Refuse a capsulod that cannot run on the service: not an ELF, not 64-bit,
|
|
31
|
+
* not aarch64. A wrong file makes the build die at /ready with no log at all.
|
|
32
|
+
*/
|
|
33
|
+
export declare function checkCapsulod(path: string): void;
|
|
34
|
+
/** Build the zip the image builder consumes: context + Dockerfile + capsulod at the root. */
|
|
35
|
+
export declare function assembleArtifact(opts: TemplateBuildOptions, capsulod: string): Buffer;
|
|
36
|
+
export declare function templateBuild(cfg: CapsulosConfig, opts: TemplateBuildOptions): Promise<{
|
|
37
|
+
imageArn: string;
|
|
38
|
+
version: string;
|
|
39
|
+
}>;
|
package/dist/template.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// `capsulos template build <name>`: a Dockerfile becomes a MicrovmImage.
|
|
2
|
+
// 1. assemble a build context: the user's directory + Dockerfile + capsulod
|
|
3
|
+
// 2. zip it and upload to the artifact bucket
|
|
4
|
+
// 3. CreateMicrovmImage (or UpdateMicrovmImage → new version) with the
|
|
5
|
+
// /ready + /run hooks and, when asked, additionalOsCapabilities=ALL (mounts)
|
|
6
|
+
// 4. poll until CREATED/UPDATED
|
|
7
|
+
// The Dockerfile must end up running capsulod on :8080 (see templates/base).
|
|
8
|
+
import { execFileSync } from 'node:child_process';
|
|
9
|
+
import { chmodSync, closeSync, cpSync, existsSync, mkdtempSync, openSync, readFileSync, readSync, rmSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
|
14
|
+
import { LambdaMicrovmsClient, CreateMicrovmImageCommand, UpdateMicrovmImageCommand, GetMicrovmImageCommand, } from '@aws-sdk/client-lambda-microvms';
|
|
15
|
+
import { credentialsFor } from './config.js';
|
|
16
|
+
const HOOKS_PORT = 9000;
|
|
17
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
18
|
+
export function imageArn(cfg, name) {
|
|
19
|
+
return `arn:aws:lambda:${cfg.region}:${cfg.accountId}:microvm-image:${name}`;
|
|
20
|
+
}
|
|
21
|
+
/** Resolve or cross-compile capsulod for linux/arm64 (the service runs Graviton). */
|
|
22
|
+
export function resolveCapsulod(opts) {
|
|
23
|
+
if (opts.capsulod) {
|
|
24
|
+
if (!existsSync(opts.capsulod))
|
|
25
|
+
throw new Error(`capsulod binary not found: ${opts.capsulod}`);
|
|
26
|
+
return resolve(opts.capsulod);
|
|
27
|
+
}
|
|
28
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const src = opts.daemonSrc ?? resolve(here, '..', '..', '..', 'daemon');
|
|
30
|
+
if (!existsSync(join(src, 'go.mod'))) {
|
|
31
|
+
throw new Error(`no capsulod binary given and no daemon source at ${src}; pass --capsulod <path> (download capsulod-linux-arm64 from https://github.com/tornoai/capsulos/releases) or --daemon-src <dir>`);
|
|
32
|
+
}
|
|
33
|
+
const out = join(mkdtempSync(join(tmpdir(), 'capsulod-')), 'capsulod');
|
|
34
|
+
opts.log?.(`building capsulod for linux/arm64 from ${src}`);
|
|
35
|
+
execFileSync('go', ['build', '-trimpath', '-ldflags', '-s -w', '-o', out, './cmd/capsulod'], {
|
|
36
|
+
cwd: src,
|
|
37
|
+
env: { ...process.env, CGO_ENABLED: '0', GOOS: 'linux', GOARCH: 'arm64' },
|
|
38
|
+
stdio: 'inherit',
|
|
39
|
+
});
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Refuse a capsulod that cannot run on the service: not an ELF, not 64-bit,
|
|
44
|
+
* not aarch64. A wrong file makes the build die at /ready with no log at all.
|
|
45
|
+
*/
|
|
46
|
+
export function checkCapsulod(path) {
|
|
47
|
+
const head = Buffer.alloc(20);
|
|
48
|
+
const fd = openSync(path, 'r');
|
|
49
|
+
let n = 0;
|
|
50
|
+
try {
|
|
51
|
+
n = readSync(fd, head, 0, 20, 0);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
const why = (() => {
|
|
57
|
+
if (n < 20 || head.readUInt32BE(0) !== 0x7f454c46)
|
|
58
|
+
return 'not an ELF binary';
|
|
59
|
+
if (head[4] !== 2)
|
|
60
|
+
return 'not a 64-bit binary';
|
|
61
|
+
if (head.readUInt16LE(18) !== 0xb7)
|
|
62
|
+
return 'not built for aarch64 (Lambda MicroVMs run Graviton)';
|
|
63
|
+
return null;
|
|
64
|
+
})();
|
|
65
|
+
if (why) {
|
|
66
|
+
throw new Error(`capsulod at ${path} is ${why}; download capsulod-linux-arm64 from https://github.com/tornoai/capsulos/releases or build it with GOOS=linux GOARCH=arm64`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Build the zip the image builder consumes: context + Dockerfile + capsulod at the root. */
|
|
70
|
+
export function assembleArtifact(opts, capsulod) {
|
|
71
|
+
const context = resolve(opts.context);
|
|
72
|
+
const dockerfile = resolve(opts.dockerfile ?? join(context, 'Dockerfile'));
|
|
73
|
+
if (!existsSync(dockerfile))
|
|
74
|
+
throw new Error(`Dockerfile not found: ${dockerfile}`);
|
|
75
|
+
const stage = mkdtempSync(join(tmpdir(), 'capsulos-template-'));
|
|
76
|
+
try {
|
|
77
|
+
cpSync(context, stage, { recursive: true });
|
|
78
|
+
cpSync(dockerfile, join(stage, 'Dockerfile'));
|
|
79
|
+
cpSync(capsulod, join(stage, 'capsulod'));
|
|
80
|
+
// A downloaded release asset comes as 0644; COPY keeps the mode and CMD ["capsulod"] then cannot run.
|
|
81
|
+
chmodSync(join(stage, 'capsulod'), 0o755);
|
|
82
|
+
const zipPath = join(stage, '..', `capsulos-${opts.name}.zip`);
|
|
83
|
+
rmSync(zipPath, { force: true });
|
|
84
|
+
execFileSync('zip', ['-q', '-r', '-X', zipPath, '.'], { cwd: stage });
|
|
85
|
+
return readFileSync(zipPath);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
rmSync(stage, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function templateBuild(cfg, opts) {
|
|
92
|
+
const log = opts.log ?? (() => { });
|
|
93
|
+
const credentials = credentialsFor(opts.profile);
|
|
94
|
+
const s3 = new S3Client({ region: cfg.region, credentials });
|
|
95
|
+
const mvm = new LambdaMicrovmsClient({ region: cfg.region, credentials });
|
|
96
|
+
const capsulod = resolveCapsulod(opts);
|
|
97
|
+
checkCapsulod(capsulod);
|
|
98
|
+
const zip = assembleArtifact(opts, capsulod);
|
|
99
|
+
const key = `templates/${opts.name}.zip`;
|
|
100
|
+
await s3.send(new PutObjectCommand({ Bucket: cfg.bucket, Key: key, Body: zip }));
|
|
101
|
+
log(`uploaded s3://${cfg.bucket}/${key} (${zip.length} bytes)`);
|
|
102
|
+
const arn = imageArn(cfg, opts.name);
|
|
103
|
+
const capabilities = opts.capabilities ?? cfg.capabilities ?? 'default';
|
|
104
|
+
log(`os capabilities: ${capabilities}${capabilities === 'all' ? ' (kernel mounts allowed, guest less hardened)' : ' (no kernel mounts; pass --capabilities all for tmpfs/overlay/S3)'}`);
|
|
105
|
+
const spec = {
|
|
106
|
+
codeArtifact: { uri: `s3://${cfg.bucket}/${key}` },
|
|
107
|
+
baseImageArn: `arn:aws:lambda:${cfg.region}:aws:microvm-image:al2023-1`,
|
|
108
|
+
buildRoleArn: cfg.buildRoleArn,
|
|
109
|
+
cpuConfigurations: [{ architecture: 'ARM_64' }],
|
|
110
|
+
resources: [{ minimumMemoryInMiB: opts.memoryMiB ?? 512 }],
|
|
111
|
+
// Verified: without this the capsule has Docker's capability set and
|
|
112
|
+
// cannot mount anything (docs/lambda-microvms-findings.md); with it,
|
|
113
|
+
// every process inside runs with all capabilities and no seccomp.
|
|
114
|
+
...(capabilities === 'all' ? { additionalOsCapabilities: ['ALL'] } : {}),
|
|
115
|
+
hooks: {
|
|
116
|
+
port: HOOKS_PORT,
|
|
117
|
+
microvmHooks: { run: 'ENABLED', runTimeoutInSeconds: 60 },
|
|
118
|
+
microvmImageHooks: { ready: 'ENABLED', readyTimeoutInSeconds: 120 },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
const t0 = Date.now();
|
|
122
|
+
try {
|
|
123
|
+
await mvm.send(new CreateMicrovmImageCommand({
|
|
124
|
+
name: opts.name,
|
|
125
|
+
description: opts.description ?? `capsulos template ${opts.name}`,
|
|
126
|
+
tags: opts.tags ?? cfg.tags ?? { project: 'capsulos' },
|
|
127
|
+
...spec,
|
|
128
|
+
}));
|
|
129
|
+
log(`image ${arn} created; building…`);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (err instanceof Error && err.name === 'ConflictException') {
|
|
133
|
+
await mvm.send(new UpdateMicrovmImageCommand({ imageIdentifier: arn, ...spec }));
|
|
134
|
+
log(`image ${arn} exists → new version; building…`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
for (;;) {
|
|
141
|
+
await sleep(10_000);
|
|
142
|
+
const img = await mvm.send(new GetMicrovmImageCommand({ imageIdentifier: arn }));
|
|
143
|
+
const elapsed = ((Date.now() - t0) / 1000).toFixed(0);
|
|
144
|
+
log(`state=${img.state} (t+${elapsed}s)`);
|
|
145
|
+
if (img.state === 'CREATED' || img.state === 'UPDATED') {
|
|
146
|
+
return { imageArn: arn, version: String(img.latestActiveImageVersion ?? '') };
|
|
147
|
+
}
|
|
148
|
+
if (img.state?.endsWith('_FAILED')) {
|
|
149
|
+
throw new Error(`build failed: ${img.state} — see CloudWatch /aws/lambda-microvms/${opts.name}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.js","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,8EAA8E;AAC9E,gDAAgD;AAChD,yEAAyE;AACzE,kFAAkF;AAClF,kCAAkC;AAClC,6EAA6E;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC1H,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EACL,oBAAoB,EACpB,yBAAyB,EACzB,yBAAyB,EACzB,sBAAsB,GACvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,cAAc,EAAuB,MAAM,aAAa,CAAC;AA4BlE,MAAM,UAAU,GAAG,IAAI,CAAC;AACxB,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAEpE,MAAM,UAAU,QAAQ,CAAC,GAAmB,EAAE,IAAY;IACxD,OAAO,kBAAkB,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,SAAS,kBAAkB,IAAI,EAAE,CAAC;AAC/E,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,eAAe,CAAC,IAAkE;IAChG,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC/F,OAAO,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,IAAI,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IACxE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,oDAAoD,GAAG,kIAAkI,CAC1L,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,WAAW,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IACvE,IAAI,CAAC,GAAG,EAAE,CAAC,0CAA0C,GAAG,EAAE,CAAC,CAAC;IAC5D,YAAY,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,gBAAgB,CAAC,EAAE;QAC3F,GAAG,EAAE,GAAG;QACR,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;QACzE,KAAK,EAAE,SAAS;KACjB,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC9B,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC/B,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,IAAI,CAAC;QACH,CAAC,GAAG,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IACnC,CAAC;YAAS,CAAC;QACT,SAAS,CAAC,EAAE,CAAC,CAAC;IAChB,CAAC;IACD,MAAM,GAAG,GAAG,CAAC,GAAG,EAAE;QAChB,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,UAAU;YAAE,OAAO,mBAAmB,CAAC;QAC9E,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO,qBAAqB,CAAC;QAChD,IAAI,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,KAAK,IAAI;YAAE,OAAO,sDAAsD,CAAC;QAClG,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,EAAE,CAAC;IACL,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,IAAI,KAAK,CACb,eAAe,IAAI,OAAO,GAAG,4HAA4H,CAC1J,CAAC;IACJ,CAAC;AACH,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,gBAAgB,CAAC,IAA0B,EAAE,QAAgB;IAC3E,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;IAC3E,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,UAAU,EAAE,CAAC,CAAC;IACpF,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,oBAAoB,CAAC,CAAC,CAAC;IAChE,IAAI,CAAC;QACH,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5C,MAAM,CAAC,UAAU,EAAE,IAAI,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC;QAC9C,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC;QAC1C,sGAAsG;QACtG,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,CAAC;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,YAAY,IAAI,CAAC,IAAI,MAAM,CAAC,CAAC;QAC/D,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACjC,YAAY,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,OAAO,YAAY,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAmB,EAAE,IAA0B;IACjF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACnC,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACjD,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IAC7D,MAAM,GAAG,GAAG,IAAI,oBAAoB,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;IAE1E,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IACvC,aAAa,CAAC,QAAQ,CAAC,CAAC;IACxB,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC7C,MAAM,GAAG,GAAG,aAAa,IAAI,CAAC,IAAI,MAAM,CAAC;IACzC,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,gBAAgB,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IACjF,GAAG,CAAC,iBAAiB,GAAG,CAAC,MAAM,IAAI,GAAG,KAAK,GAAG,CAAC,MAAM,SAAS,CAAC,CAAC;IAEhE,MAAM,GAAG,GAAG,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,YAAY,GAAiB,IAAI,CAAC,YAAY,IAAI,GAAG,CAAC,YAAY,IAAI,SAAS,CAAC;IACtF,GAAG,CAAC,oBAAoB,YAAY,GAAG,YAAY,KAAK,KAAK,CAAC,CAAC,CAAC,+CAA+C,CAAC,CAAC,CAAC,mEAAmE,EAAE,CAAC,CAAC;IACzL,MAAM,IAAI,GAAG;QACX,YAAY,EAAE,EAAE,GAAG,EAAE,QAAQ,GAAG,CAAC,MAAM,IAAI,GAAG,EAAE,EAAE;QAClD,YAAY,EAAE,kBAAkB,GAAG,CAAC,MAAM,6BAA6B;QACvE,YAAY,EAAE,GAAG,CAAC,YAAY;QAC9B,iBAAiB,EAAE,CAAC,EAAE,YAAY,EAAE,QAAiB,EAAE,CAAC;QACxD,SAAS,EAAE,CAAC,EAAE,kBAAkB,EAAE,IAAI,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC;QAC1D,qEAAqE;QACrE,qEAAqE;QACrE,kEAAkE;QAClE,GAAG,CAAC,YAAY,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,wBAAwB,EAAE,CAAC,KAAc,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjF,KAAK,EAAE;YACL,IAAI,EAAE,UAAU;YAChB,YAAY,EAAE,EAAE,GAAG,EAAE,SAAkB,EAAE,mBAAmB,EAAE,EAAE,EAAE;YAClE,iBAAiB,EAAE,EAAE,KAAK,EAAE,SAAkB,EAAE,qBAAqB,EAAE,GAAG,EAAE;SAC7E;KACF,CAAC;IAEF,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACtB,IAAI,CAAC;QACH,MAAM,GAAG,CAAC,IAAI,CACZ,IAAI,yBAAyB,CAAC;YAC5B,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,qBAAqB,IAAI,CAAC,IAAI,EAAE;YACjE,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE;YACtD,GAAG,IAAI;SACR,CAAC,CACH,CAAC;QACF,GAAG,CAAC,SAAS,GAAG,qBAAqB,CAAC,CAAC;IACzC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,mBAAmB,EAAE,CAAC;YAC7D,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,yBAAyB,CAAC,EAAE,eAAe,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;YACjF,GAAG,CAAC,SAAS,GAAG,kCAAkC,CAAC,CAAC;QACtD,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,SAAS,CAAC;QACR,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC;QACpB,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,sBAAsB,CAAC,EAAE,eAAe,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;QACjF,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QACtD,GAAG,CAAC,SAAS,GAAG,CAAC,KAAK,OAAO,OAAO,IAAI,CAAC,CAAC;QAC1C,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,IAAI,GAAG,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YACvD,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,wBAAwB,IAAI,EAAE,CAAC,EAAE,CAAC;QAChF,CAAC;QACD,IAAI,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,KAAK,0CAA0C,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACnG,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capsulos/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "capsulos command line: set up your AWS account, build templates, list and kill capsules.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"sandbox",
|
|
8
|
+
"ai-agents",
|
|
9
|
+
"aws",
|
|
10
|
+
"lambda",
|
|
11
|
+
"microvm",
|
|
12
|
+
"firecracker",
|
|
13
|
+
"e2b",
|
|
14
|
+
"daytona"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/tornoai/capsulos#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/tornoai/capsulos.git",
|
|
20
|
+
"directory": "packages/cli"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/tornoai/capsulos/issues"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"bin": {
|
|
30
|
+
"capsulos": "./bin/capsulos.js"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"main": "./dist/index.js",
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"files": [
|
|
42
|
+
"bin",
|
|
43
|
+
"dist",
|
|
44
|
+
"src"
|
|
45
|
+
],
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@aws-sdk/client-iam": "^3.1158.0",
|
|
51
|
+
"@aws-sdk/client-lambda-microvms": "^3.1158.0",
|
|
52
|
+
"@aws-sdk/client-s3": "^3.1158.0",
|
|
53
|
+
"@aws-sdk/client-sts": "^3.1158.0",
|
|
54
|
+
"@aws-sdk/credential-providers": "^3.1158.0",
|
|
55
|
+
"@capsulos/provider-docker": "0.1.1",
|
|
56
|
+
"@capsulos/provider-lambda": "0.1.1",
|
|
57
|
+
"@capsulos/sdk": "0.1.1"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/node": "^22.20.1",
|
|
61
|
+
"tsx": "^4.23.1",
|
|
62
|
+
"typescript": "^5.9.0",
|
|
63
|
+
"vitest": "^4.1.10"
|
|
64
|
+
},
|
|
65
|
+
"scripts": {
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"build": "tsc -p tsconfig.build.json",
|
|
68
|
+
"test": "vitest run --passWithNoTests",
|
|
69
|
+
"dev": "tsx src/cli.ts"
|
|
70
|
+
}
|
|
6
71
|
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// capsulos <command>
|
|
2
|
+
// init set up the AWS account (bucket + roles) → capsulos.json
|
|
3
|
+
// template build Dockerfile → MicrovmImage
|
|
4
|
+
// run <template> create a capsule, print its id
|
|
5
|
+
// ls <template> capsules of a template
|
|
6
|
+
// shell <id> attach your terminal to a capsule
|
|
7
|
+
// forward <id> <port> http://localhost:<port> → that port inside the capsule
|
|
8
|
+
// kill <id> terminate a capsule
|
|
9
|
+
// `--docker` on run/ls/shell/kill uses the local Docker provider instead of Lambda.
|
|
10
|
+
import { parseArgs } from 'node:util';
|
|
11
|
+
import { DockerProvider } from '@capsulos/provider-docker';
|
|
12
|
+
import { LambdaProvider } from '@capsulos/provider-lambda';
|
|
13
|
+
import { Capsule, type Provider } from '@capsulos/sdk';
|
|
14
|
+
import { credentialsFor, loadConfig } from './config.js';
|
|
15
|
+
import { forward } from './forward.js';
|
|
16
|
+
import { init } from './init.js';
|
|
17
|
+
import { shell } from './shell.js';
|
|
18
|
+
import { templateBuild } from './template.js';
|
|
19
|
+
|
|
20
|
+
const log = (msg: string) => console.error(`[capsulos] ${msg}`);
|
|
21
|
+
|
|
22
|
+
function usage(): never {
|
|
23
|
+
console.error(`usage:
|
|
24
|
+
capsulos init [--region us-east-1] [--profile p] [--s3-bucket b ...] [--egress-connector arn]
|
|
25
|
+
capsulos template build <name> --context <dir> [--dockerfile f] [--capsulod bin | --daemon-src dir] [--memory 512]
|
|
26
|
+
[--capabilities default|all] [--tag k=v ...] [--profile p]
|
|
27
|
+
capsulos run <template> [--docker | --profile p]
|
|
28
|
+
capsulos ls <template> [--docker | --profile p]
|
|
29
|
+
capsulos shell <id> [--cmd /bin/bash] [--docker | --profile p]
|
|
30
|
+
capsulos forward <id> <port> [--local-port p] [--docker | --profile p]
|
|
31
|
+
capsulos kill <id> [--docker | --profile p]`);
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const providerFlags = { profile: { type: 'string' }, docker: { type: 'boolean', default: false } } as const;
|
|
36
|
+
|
|
37
|
+
function provider(opts: { profile?: string; docker?: boolean }): Provider {
|
|
38
|
+
if (opts.docker) return new DockerProvider();
|
|
39
|
+
return lambdaProvider(opts.profile);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function lambdaProvider(profile?: string): LambdaProvider {
|
|
43
|
+
const cfg = loadConfig();
|
|
44
|
+
return new LambdaProvider({
|
|
45
|
+
region: cfg.region,
|
|
46
|
+
accountId: cfg.accountId,
|
|
47
|
+
credentials: credentialsFor(profile),
|
|
48
|
+
executionRoleArn: cfg.executionRoleArn,
|
|
49
|
+
...(cfg.egressConnectorArn ? { egressConnectorArn: cfg.egressConnectorArn } : {}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function main(argv = process.argv.slice(2)): Promise<void> {
|
|
54
|
+
const [cmd, ...rest] = argv;
|
|
55
|
+
switch (cmd) {
|
|
56
|
+
case 'init': {
|
|
57
|
+
const { values } = parseArgs({
|
|
58
|
+
args: rest,
|
|
59
|
+
options: {
|
|
60
|
+
region: { type: 'string', default: process.env.AWS_REGION ?? 'us-east-1' },
|
|
61
|
+
profile: { type: 'string' },
|
|
62
|
+
's3-bucket': { type: 'string', multiple: true },
|
|
63
|
+
'egress-connector': { type: 'string' },
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const cfg = await init({
|
|
67
|
+
region: values.region!,
|
|
68
|
+
profile: values.profile,
|
|
69
|
+
s3Buckets: values['s3-bucket'],
|
|
70
|
+
egressConnectorArn: values['egress-connector'],
|
|
71
|
+
log,
|
|
72
|
+
});
|
|
73
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
case 'template': {
|
|
77
|
+
const [sub, name, ...args] = rest;
|
|
78
|
+
if (sub !== 'build' || !name) usage();
|
|
79
|
+
const { values } = parseArgs({
|
|
80
|
+
args,
|
|
81
|
+
options: {
|
|
82
|
+
context: { type: 'string' },
|
|
83
|
+
dockerfile: { type: 'string' },
|
|
84
|
+
capsulod: { type: 'string' },
|
|
85
|
+
'daemon-src': { type: 'string' },
|
|
86
|
+
memory: { type: 'string' },
|
|
87
|
+
capabilities: { type: 'string' },
|
|
88
|
+
tag: { type: 'string', multiple: true },
|
|
89
|
+
profile: { type: 'string' },
|
|
90
|
+
description: { type: 'string' },
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
if (!values.context) usage();
|
|
94
|
+
if (values.capabilities && values.capabilities !== 'default' && values.capabilities !== 'all') usage();
|
|
95
|
+
const tags = values.tag?.length
|
|
96
|
+
? Object.fromEntries(values.tag.map((t) => { const i = t.indexOf('='); return i > 0 ? [t.slice(0, i), t.slice(i + 1)] : usage(); }))
|
|
97
|
+
: undefined;
|
|
98
|
+
const res = await templateBuild(loadConfig(), {
|
|
99
|
+
name,
|
|
100
|
+
context: values.context,
|
|
101
|
+
dockerfile: values.dockerfile,
|
|
102
|
+
capsulod: values.capsulod,
|
|
103
|
+
daemonSrc: values['daemon-src'],
|
|
104
|
+
memoryMiB: values.memory ? Number(values.memory) : undefined,
|
|
105
|
+
capabilities: values.capabilities as 'default' | 'all' | undefined,
|
|
106
|
+
tags,
|
|
107
|
+
profile: values.profile,
|
|
108
|
+
description: values.description,
|
|
109
|
+
log,
|
|
110
|
+
});
|
|
111
|
+
console.log(JSON.stringify(res, null, 2));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
case 'run': {
|
|
115
|
+
const [template, ...args] = rest;
|
|
116
|
+
if (!template) usage();
|
|
117
|
+
const { values } = parseArgs({ args, options: providerFlags });
|
|
118
|
+
const t0 = Date.now();
|
|
119
|
+
const capsule = await Capsule.create(provider(values), {
|
|
120
|
+
template,
|
|
121
|
+
idle: { suspendAfterSeconds: 600, destroyAfterSeconds: 4 * 3600 },
|
|
122
|
+
});
|
|
123
|
+
log(`capsule ready in ${Date.now() - t0}ms`);
|
|
124
|
+
console.log(capsule.id);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
case 'ls': {
|
|
128
|
+
const [template, ...args] = rest;
|
|
129
|
+
if (!template) usage();
|
|
130
|
+
const { values } = parseArgs({ args, options: providerFlags });
|
|
131
|
+
const list = await provider(values).list(template);
|
|
132
|
+
for (const c of list) console.log(`${c.id}\t${c.state}\t${c.createdAt?.toISOString() ?? ''}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
case 'shell': {
|
|
136
|
+
const [id, ...args] = rest;
|
|
137
|
+
if (!id) usage();
|
|
138
|
+
const { values } = parseArgs({ args, options: { ...providerFlags, cmd: { type: 'string' } } });
|
|
139
|
+
const code = await shell(provider(values), id, values.cmd);
|
|
140
|
+
process.exit(code);
|
|
141
|
+
}
|
|
142
|
+
case 'forward': {
|
|
143
|
+
const [id, portStr, ...args] = rest;
|
|
144
|
+
const port = Number(portStr);
|
|
145
|
+
if (!id || !Number.isInteger(port) || port < 1) usage();
|
|
146
|
+
const { values } = parseArgs({ args, options: { ...providerFlags, 'local-port': { type: 'string' } } });
|
|
147
|
+
const { url } = await forward(provider(values), id, port, {
|
|
148
|
+
localPort: values['local-port'] ? Number(values['local-port']) : undefined,
|
|
149
|
+
log,
|
|
150
|
+
});
|
|
151
|
+
console.log(url);
|
|
152
|
+
await new Promise<void>((resolve) => process.once('SIGINT', resolve));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
case 'kill': {
|
|
156
|
+
const [id, ...args] = rest;
|
|
157
|
+
if (!id) usage();
|
|
158
|
+
const { values } = parseArgs({ args, options: providerFlags });
|
|
159
|
+
await provider(values).terminate(id);
|
|
160
|
+
log(`terminated ${id}`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
default:
|
|
164
|
+
usage();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
main().catch((err) => {
|
|
169
|
+
console.error(err instanceof Error ? err.message : err);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// capsulos.json: what `capsulos init` created in the AWS account, read by
|
|
2
|
+
// every other command and by the examples. Lives in the project directory.
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
|
|
6
|
+
import type { LambdaMicrovmsClientConfig } from '@aws-sdk/client-lambda-microvms';
|
|
7
|
+
|
|
8
|
+
export type Credentials = NonNullable<LambdaMicrovmsClientConfig['credentials']>;
|
|
9
|
+
|
|
10
|
+
export interface CapsulosConfig {
|
|
11
|
+
region: string;
|
|
12
|
+
accountId: string;
|
|
13
|
+
/** Artifact bucket for template zips. */
|
|
14
|
+
bucket: string;
|
|
15
|
+
/** Role the image builder assumes (reads the zip, writes build logs). */
|
|
16
|
+
buildRoleArn: string;
|
|
17
|
+
/** Role every capsule assumes (S3 mounts and whatever else you grant). */
|
|
18
|
+
executionRoleArn: string;
|
|
19
|
+
/** Optional egress connector (e.g. a VPC connector with a deny-all security group). */
|
|
20
|
+
egressConnectorArn?: string;
|
|
21
|
+
/** Default OS capabilities for `template build` ('default' | 'all'); the flag wins. */
|
|
22
|
+
capabilities?: 'default' | 'all';
|
|
23
|
+
/** Default tags for the images `template build` creates; the flags win. */
|
|
24
|
+
tags?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const CONFIG_FILE = 'capsulos.json';
|
|
28
|
+
|
|
29
|
+
export function configPath(cwd = process.cwd()): string {
|
|
30
|
+
return resolve(cwd, CONFIG_FILE);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function loadConfig(cwd = process.cwd()): CapsulosConfig {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(configPath(cwd), 'utf8')) as CapsulosConfig;
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Error(`${CONFIG_FILE} not found in ${cwd}. Run \`capsulos init\` first.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function saveConfig(cfg: CapsulosConfig, cwd = process.cwd()): string {
|
|
42
|
+
const p = configPath(cwd);
|
|
43
|
+
writeFileSync(p, `${JSON.stringify(cfg, null, 2)}\n`);
|
|
44
|
+
return p;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Env vars first (`aws configure export-credentials`), then the profile. */
|
|
48
|
+
export function credentialsFor(profile?: string): Credentials {
|
|
49
|
+
if (process.env.AWS_ACCESS_KEY_ID || !profile) return fromNodeProviderChain();
|
|
50
|
+
return fromNodeProviderChain({ profile });
|
|
51
|
+
}
|