vmsan 0.1.0-alpha.2 → 0.1.0-alpha.21

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.
@@ -1,4 +1,4 @@
1
- import { A as invalidDomainError, D as invalidCidrPrefixError, E as invalidCidrOctetError, F as invalidIntegerFlagError, I as invalidNetworkPolicyError, L as invalidPortError, N as invalidImageRefEmptyError, O as invalidDiskSizeFormatError, P as invalidImageRefTagError, R as invalidRuntimeError, T as invalidCidrFormatError, V as portConflictError, j as invalidDomainPatternError, k as invalidDiskSizeRangeError } from "./errors.mjs";
1
+ import { A as invalidDiskSizeRangeError, D as invalidCidrOctetError, E as invalidCidrFormatError, F as invalidImageRefTagError, H as portConflictError, I as invalidIntegerFlagError, L as invalidNetworkPolicyError, M as invalidDomainPatternError, O as invalidCidrPrefixError, P as invalidImageRefEmptyError, R as invalidPortError, j as invalidDomainError, k as invalidDiskSizeFormatError, z as invalidRuntimeError } from "./errors.mjs";
2
2
  import { t as FileVmStateStore } from "./vm-state.mjs";
3
3
  const VALID_RUNTIMES = [
4
4
  "base",
@@ -0,0 +1,34 @@
1
+ import { S as vmNotRunningError, b as vmNoAgentTokenError, l as agentTimeoutError, x as vmNotFoundError } from "./errors.mjs";
2
+ import { t as FileVmStateStore } from "./vm-state.mjs";
3
+ /**
4
+ * Load VM state and validate it's running with an agent token.
5
+ * Throws VmError on failure (handled by handleCommandError in the caller).
6
+ */
7
+ function resolveVmState(vmId, paths) {
8
+ const store = new FileVmStateStore(paths.vmsDir);
9
+ const state = store.load(vmId);
10
+ if (!state) throw vmNotFoundError(vmId);
11
+ if (state.status !== "running") throw vmNotRunningError(vmId, state.status);
12
+ if (!state.agentToken) throw vmNoAgentTokenError(vmId);
13
+ return {
14
+ state,
15
+ guestIp: state.network.guestIp,
16
+ port: state.agentPort || paths.agentPort,
17
+ store
18
+ };
19
+ }
20
+ /**
21
+ * Poll the agent health endpoint until it responds OK.
22
+ */
23
+ async function waitForAgent(guestIp, port, timeoutMs = 6e4) {
24
+ const start = Date.now();
25
+ const url = `http://${guestIp}:${port}/health`;
26
+ while (Date.now() - start < timeoutMs) {
27
+ try {
28
+ if ((await fetch(url, { signal: AbortSignal.timeout(2e3) })).ok) return;
29
+ } catch {}
30
+ await new Promise((r) => setTimeout(r, 500));
31
+ }
32
+ throw agentTimeoutError(guestIp, timeoutMs);
33
+ }
34
+ export { waitForAgent as n, resolveVmState as t };
@@ -1,10 +1,36 @@
1
- import { C as vmStateNotFoundError, M as invalidDurationError, v as networkSlotsExhaustedError } from "./errors.mjs";
1
+ import { N as invalidDurationError, v as networkSlotsExhaustedError, w as vmStateNotFoundError } from "./errors.mjs";
2
2
  import { join } from "node:path";
3
3
  import { execSync } from "node:child_process";
4
- import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { chmodSync, chownSync, existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { stripAnsi } from "consola/utils";
7
7
  /**
8
+ * Resolve the UID/GID of the real (non-root) user when running under sudo.
9
+ * Returns null when not running under sudo or when env vars are missing.
10
+ */
11
+ function getSudoOwner() {
12
+ const uid = process.env.SUDO_UID;
13
+ const gid = process.env.SUDO_GID;
14
+ if (!uid || !gid) return null;
15
+ const numUid = Number(uid);
16
+ const numGid = Number(gid);
17
+ if (!Number.isInteger(numUid) || !Number.isInteger(numGid)) return null;
18
+ return {
19
+ uid: numUid,
20
+ gid: numGid
21
+ };
22
+ }
23
+ function chownToSudoUser(path) {
24
+ const owner = getSudoOwner();
25
+ if (!owner) return;
26
+ try {
27
+ chownSync(path, owner.uid, owner.gid);
28
+ } catch {}
29
+ }
30
+ function toError(err) {
31
+ return err instanceof Error ? err : new Error(String(err));
32
+ }
33
+ /**
8
34
  * Send a signal to a process. Returns true if delivered, false if
9
35
  * the process is already dead (ESRCH). Falls back to sudo for
10
36
  * root-owned processes (EPERM). Re-throws unexpected errors.
@@ -80,6 +106,7 @@ function mkdirSecure(path) {
80
106
  } catch (error) {
81
107
  if (error.code !== "ENOENT") throw error;
82
108
  }
109
+ chownToSudoUser(path);
83
110
  }
84
111
  function writeSecure(path, contents) {
85
112
  writeFileSync(path, contents, { mode: 384 });
@@ -88,6 +115,7 @@ function writeSecure(path, contents) {
88
115
  } catch (error) {
89
116
  if (error.code !== "ENOENT") throw error;
90
117
  }
118
+ chownToSudoUser(path);
91
119
  }
92
120
  const TIME_UNITS = [
93
121
  [
@@ -150,7 +178,16 @@ function table(opts) {
150
178
  const sep = " ";
151
179
  return [`\x1b[1m${titles.map(padded).join(sep)}\x1b[0m`, ...data.map((row) => row.map(padded).join(sep))].join("\n");
152
180
  }
153
- var FileVmStateStore = class FileVmStateStore {
181
+ function findFreeNetworkSlot(states) {
182
+ const usedSlots = new Set(states.filter((s) => s.status === "running" || s.status === "creating").map((s) => {
183
+ const parts = s.network.hostIp.split(".");
184
+ return Number(parts[2]);
185
+ }));
186
+ for (const slot of getActiveTapSlots()) usedSlots.add(slot);
187
+ for (let slot = 0; slot <= 254; slot++) if (!usedSlots.has(slot)) return slot;
188
+ throw networkSlotsExhaustedError();
189
+ }
190
+ var FileVmStateStore = class {
154
191
  constructor(dir) {
155
192
  this.dir = dir;
156
193
  }
@@ -181,26 +218,21 @@ var FileVmStateStore = class FileVmStateStore {
181
218
  if (existsSync(filePath)) unlinkSync(filePath);
182
219
  }
183
220
  allocateNetworkSlot() {
184
- const states = this.list();
185
- const usedSlots = new Set(states.filter((s) => s.status === "running" || s.status === "creating").map((s) => {
186
- const parts = s.network.hostIp.split(".");
187
- return Number(parts[2]);
188
- }));
189
- for (const slot of FileVmStateStore.getActiveTapSlots()) usedSlots.add(slot);
190
- for (let slot = 0; slot <= 254; slot++) if (!usedSlots.has(slot)) return slot;
191
- throw networkSlotsExhaustedError();
192
- }
193
- static getActiveTapSlots() {
194
- const slots = /* @__PURE__ */ new Set();
195
- try {
196
- for (const iface of readdirSync("/sys/class/net")) {
197
- const match = /^fhvm(\d+)$/.exec(iface);
198
- if (!match) continue;
199
- const slot = Number(match[1]);
200
- if (Number.isInteger(slot) && slot >= 0 && slot <= 254) slots.add(slot);
201
- }
202
- } catch {}
203
- return slots;
221
+ return findFreeNetworkSlot(this.list());
204
222
  }
205
223
  };
206
- export { parseDuration as a, timeAgo as c, mkdirSecure as i, timeRemaining as l, generateVmId as n, safeKill as o, isProcessAlive as r, table as s, FileVmStateStore as t, writeSecure as u };
224
+ function getActiveTapSlots() {
225
+ const slots = /* @__PURE__ */ new Set();
226
+ try {
227
+ for (const iface of readdirSync("/sys/class/net")) {
228
+ const tapMatch = /^fhvm(\d+)$/.exec(iface);
229
+ const vethMatch = /^veth-h-(\d+)$/.exec(iface);
230
+ const match = tapMatch || vethMatch;
231
+ if (!match) continue;
232
+ const slot = Number(match[1]);
233
+ if (Number.isInteger(slot) && slot >= 0 && slot <= 254) slots.add(slot);
234
+ }
235
+ } catch {}
236
+ return slots;
237
+ }
238
+ export { isProcessAlive as a, safeKill as c, timeRemaining as d, toError as f, generateVmId as i, table as l, findFreeNetworkSlot as n, mkdirSecure as o, writeSecure as p, getActiveTapSlots as r, parseDuration as s, FileVmStateStore as t, timeAgo as u };
package/dist/bin/cli.mjs CHANGED
@@ -1,7 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { i as initVmsanLogger } from "../_chunks/logger.mjs";
3
+ import { dirname, join } from "node:path";
4
+ import { existsSync, readFileSync } from "node:fs";
3
5
  import { consola } from "consola";
6
+ import { fileURLToPath } from "node:url";
4
7
  import { defineCommand, runMain } from "citty";
8
+ function findPackageVersion() {
9
+ let dir = dirname(fileURLToPath(import.meta.url));
10
+ while (dir !== dirname(dir)) {
11
+ const pkgPath = join(dir, "package.json");
12
+ if (existsSync(pkgPath)) return JSON.parse(readFileSync(pkgPath, "utf8")).version;
13
+ dir = dirname(dir);
14
+ }
15
+ return "0.0.0";
16
+ }
17
+ const version = findPackageVersion();
5
18
  const SUDO_COMMANDS = new Set([
6
19
  "create",
7
20
  "start",
@@ -17,7 +30,7 @@ if (subCommand && SUDO_COMMANDS.has(subCommand) && process.getuid?.() !== 0) {
17
30
  runMain(defineCommand({
18
31
  meta: {
19
32
  name: "vmsan",
20
- version: "0.1.0",
33
+ version,
21
34
  description: "Firecracker microVM sandbox toolkit"
22
35
  },
23
36
  args: {
@@ -43,9 +56,10 @@ runMain(defineCommand({
43
56
  stop: () => import("../_chunks/stop.mjs").then((m) => m.default),
44
57
  remove: () => import("../_chunks/remove.mjs").then((m) => m.default),
45
58
  rm: () => import("../_chunks/remove.mjs").then((m) => m.default),
46
- connect: () => import("../_chunks/connect2.mjs").then((m) => m.default),
59
+ connect: () => import("../_chunks/connect.mjs").then((m) => m.default),
47
60
  upload: () => import("../_chunks/upload.mjs").then((m) => m.default),
48
61
  download: () => import("../_chunks/download.mjs").then((m) => m.default),
62
+ exec: () => import("../_chunks/exec.mjs").then((m) => m.default),
49
63
  network: () => import("../_chunks/network.mjs").then((m) => m.default)
50
64
  }
51
65
  }));