rightsize 0.2.0 → 0.3.0

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.
Files changed (58) hide show
  1. package/README.md +12 -4
  2. package/dist/backend-docker/backend.d.ts +27 -4
  3. package/dist/backend-docker/backend.d.ts.map +1 -1
  4. package/dist/backend-docker/backend.js +56 -6
  5. package/dist/backend-docker/backend.js.map +1 -1
  6. package/dist/backend-docker/cli.d.ts +25 -0
  7. package/dist/backend-docker/cli.d.ts.map +1 -0
  8. package/dist/backend-docker/cli.js +64 -0
  9. package/dist/backend-docker/cli.js.map +1 -0
  10. package/dist/backend-msb/backend.d.ts +61 -7
  11. package/dist/backend-msb/backend.d.ts.map +1 -1
  12. package/dist/backend-msb/backend.js +121 -11
  13. package/dist/backend-msb/backend.js.map +1 -1
  14. package/dist/backend-msb/commands.d.ts +10 -0
  15. package/dist/backend-msb/commands.d.ts.map +1 -1
  16. package/dist/backend-msb/commands.js +29 -1
  17. package/dist/backend-msb/commands.js.map +1 -1
  18. package/dist/backend-msb/snapshot-not-found.d.ts +25 -0
  19. package/dist/backend-msb/snapshot-not-found.d.ts.map +1 -0
  20. package/dist/backend-msb/snapshot-not-found.js +27 -0
  21. package/dist/backend-msb/snapshot-not-found.js.map +1 -0
  22. package/dist/core/backend.d.ts +57 -6
  23. package/dist/core/backend.d.ts.map +1 -1
  24. package/dist/core/checkpoint/api.d.ts +80 -0
  25. package/dist/core/checkpoint/api.d.ts.map +1 -0
  26. package/dist/core/checkpoint/api.js +138 -0
  27. package/dist/core/checkpoint/api.js.map +1 -0
  28. package/dist/core/checkpoint/name.d.ts +12 -0
  29. package/dist/core/checkpoint/name.d.ts.map +1 -0
  30. package/dist/core/checkpoint/name.js +17 -0
  31. package/dist/core/checkpoint/name.js.map +1 -0
  32. package/dist/core/checkpoint/ref.d.ts +12 -0
  33. package/dist/core/checkpoint/ref.d.ts.map +1 -0
  34. package/dist/core/checkpoint/ref.js +16 -0
  35. package/dist/core/checkpoint/ref.js.map +1 -0
  36. package/dist/core/checkpoint/registry.d.ts +107 -0
  37. package/dist/core/checkpoint/registry.d.ts.map +1 -0
  38. package/dist/core/checkpoint/registry.js +180 -0
  39. package/dist/core/checkpoint/registry.js.map +1 -0
  40. package/dist/core/errors.d.ts +60 -4
  41. package/dist/core/errors.d.ts.map +1 -1
  42. package/dist/core/errors.js +80 -7
  43. package/dist/core/errors.js.map +1 -1
  44. package/dist/core/generic-container.d.ts +87 -18
  45. package/dist/core/generic-container.d.ts.map +1 -1
  46. package/dist/core/generic-container.js +171 -22
  47. package/dist/core/generic-container.js.map +1 -1
  48. package/dist/core/model.d.ts +25 -8
  49. package/dist/core/model.d.ts.map +1 -1
  50. package/dist/index.d.ts +4 -2
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +4 -2
  53. package/dist/index.js.map +1 -1
  54. package/dist/modules/keycloak.js +1 -1
  55. package/dist/modules/mysql.d.ts.map +1 -1
  56. package/dist/modules/mysql.js +4 -3
  57. package/dist/modules/mysql.js.map +1 -1
  58. package/package.json +1 -1
@@ -102,21 +102,94 @@ export class IsolationRequiredError extends Error {
102
102
  }
103
103
  /**
104
104
  * Thrown by `checkpoint()` when the active backend's
105
- * `capabilities.checkpoint` is `false` — microsandbox has no upstream
106
- * microVM snapshot support today. Thrown before any backend call: the
107
- * generic layer gates on the capability itself rather than letting the
108
- * backend's own `commitToImage` reject.
105
+ * `capabilities.checkpoint` is `false` — both real backends (docker, image
106
+ * commit; microsandbox, disk snapshot) support it today, so this only fires
107
+ * against a backend that genuinely lacks the capability (a test double).
108
+ * Thrown before any backend call: the generic layer gates on the capability
109
+ * itself rather than letting the backend's own `createCheckpoint` reject.
109
110
  */
110
111
  export class CheckpointUnsupportedError extends Error {
111
112
  backend;
112
113
  constructor(
113
114
  /** The active backend's name (e.g. `"microsandbox"`). */
114
115
  backend) {
115
- super(`checkpoint() is not supported by the '${backend}' backend — checkpoint/restore is implemented via image ` +
116
- "commit on the docker backend today; native microVM memory snapshots for microsandbox are on the " +
117
- "roadmap. Set RIGHTSIZE_BACKEND=docker to use checkpoint/restore.");
116
+ super(`checkpoint() is not supported by the '${backend}' backend — checkpointing needs a backend whose ` +
117
+ "capabilities.checkpoint is true.");
118
118
  this.backend = backend;
119
119
  this.name = "CheckpointUnsupportedError";
120
120
  }
121
121
  }
122
+ /**
123
+ * Thrown by `GenericContainer.fromCheckpoint(cp).start()` when the active
124
+ * backend's name doesn't match the backend that created `cp` — a checkpoint
125
+ * ref is only meaningful to the backend that minted it (a docker image tag
126
+ * means nothing to `msb`, and a msb snapshot name means nothing to docker).
127
+ * Thrown before any backend call.
128
+ */
129
+ export class CheckpointBackendMismatchError extends Error {
130
+ createdOnBackend;
131
+ activeBackend;
132
+ constructor(
133
+ /** The backend that created the checkpoint. */
134
+ createdOnBackend,
135
+ /** The backend `start()` actually resolved. */
136
+ activeBackend) {
137
+ super(`this checkpoint was created on the '${createdOnBackend}' backend but the active backend is ` +
138
+ `'${activeBackend}' — set RIGHTSIZE_BACKEND=${createdOnBackend} to restore it there, or create a fresh ` +
139
+ `checkpoint under the '${activeBackend}' backend instead.`);
140
+ this.createdOnBackend = createdOnBackend;
141
+ this.activeBackend = activeBackend;
142
+ this.name = "CheckpointBackendMismatchError";
143
+ }
144
+ }
145
+ /**
146
+ * Thrown at `start()` when a container built via `GenericContainer.fromCheckpoint()`
147
+ * is also marked `withReuse()` — reuse's identity hash never covers
148
+ * `checkpointRef`, so an adopted sandbox from an earlier process could never
149
+ * be verified against the checkpoint this container was meant to restore.
150
+ * Thrown only once reuse is actually double opt-in active, the same
151
+ * placement as `ReuseWithNetworkError`.
152
+ */
153
+ export class ReuseFromCheckpointError extends Error {
154
+ constructor() {
155
+ super("withReuse() cannot be combined with fromCheckpoint() — reuse's identity hash does not cover " +
156
+ "checkpointRef, so an adopted sandbox could never be verified against the checkpoint it was meant to " +
157
+ "restore. Drop either withReuse() or fromCheckpoint().");
158
+ this.name = "ReuseFromCheckpointError";
159
+ }
160
+ }
161
+ /**
162
+ * Thrown by `copyFileToContainer`/`copyContentToContainer`/
163
+ * `copyFileFromContainer` when `containerPath` is not absolute — both
164
+ * backends require an absolute `NAME:/path` shape, so a relative path can
165
+ * never reach either CLI. Thrown before any backend call.
166
+ */
167
+ export class RelativeContainerPathError extends Error {
168
+ containerPath;
169
+ constructor(
170
+ /** The rejected path, exactly as passed in. */
171
+ containerPath) {
172
+ super(`containerPath must be an absolute path (got '${containerPath}') — both backends require an absolute guest path.`);
173
+ this.containerPath = containerPath;
174
+ this.name = "RelativeContainerPathError";
175
+ }
176
+ }
177
+ /**
178
+ * Thrown by `checkpoint(name)` when `name` doesn't match
179
+ * `^[a-z0-9][a-z0-9-]{0,40}$` — the same pattern pinned across every
180
+ * rightsize language implementation. Thrown before any backend or
181
+ * filesystem call, so a bad name never mints a ref or touches the registry.
182
+ */
183
+ export class InvalidCheckpointNameError extends Error {
184
+ checkpointName;
185
+ constructor(
186
+ /** The rejected name, exactly as passed in. */
187
+ checkpointName) {
188
+ super(`checkpoint name '${checkpointName}' is invalid — a checkpoint name must match ^[a-z0-9][a-z0-9-]{0,40}$ ` +
189
+ "(start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens after " +
190
+ "that, and be at most 41 characters long).");
191
+ this.checkpointName = checkpointName;
192
+ this.name = "InvalidCheckpointNameError";
193
+ }
194
+ }
122
195
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAGvC;IAEA;IAEA;IANX;IACE,uDAAuD;IAC9C,OAAe;IACxB,yDAAyD;IAChD,OAAe;IACxB,qFAAqF;IAC5E,MAAe;QAExB,KAAK,CAAC,YAAY,OAAO,8BAA8B,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QANjG,YAAO,GAAP,OAAO,CAAQ;QAEf,YAAO,GAAP,OAAO,CAAQ;QAEf,WAAM,GAAN,MAAM,CAAS;QAGxB,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IAC1C,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAInC;IAHX,YACE,OAAe;IACf,4FAA4F;IACnF,KAAe;QAExB,KAAK,CAAC,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAFnD,UAAK,GAAL,KAAK,CAAU;QAGxB,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED,gFAAgF;AAChF,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AAED,8EAA8E;AAC9E,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF;AAED,kIAAkI;AAClI,MAAM,OAAO,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED;;;;;;;;;GASG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C;QACE,KAAK,CACH,mGAAmG;YACjG,oGAAoG;YACpG,iDAAiD,CACpD,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAGpC;IAFX;IACE,mDAAmD;IAC1C,OAAe;QAExB,KAAK,CACH,2FAA2F,OAAO,KAAK;YACrG,oGAAoG;YACpG,iFAAiF,CACpF,CAAC;QANO,YAAO,GAAP,OAAO,CAAQ;QAOxB,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,0BAA2B,SAAQ,KAAK;IAGxC;IAFX;IACE,yDAAyD;IAChD,OAAe;QAExB,KAAK,CACH,yCAAyC,OAAO,0DAA0D;YACxG,kGAAkG;YAClG,kEAAkE,CACrE,CAAC;QANO,YAAO,GAAP,OAAO,CAAQ;QAOxB,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;IAC3C,CAAC;CACF"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"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"}
@@ -35,25 +35,32 @@ export declare class GenericContainer implements AsyncDisposable, NetworkMember
35
35
  private backendOverride;
36
36
  private reuseRequested;
37
37
  private requireIsolationRequested;
38
+ private checkpointRef;
39
+ private checkpointSourceBackend;
38
40
  private handle;
39
41
  private backend;
40
42
  private mappedPorts;
41
43
  private running;
44
+ private installedNetworkLinks;
42
45
  /** Builds against `image` (e.g. `"redis:8.6-alpine"`); no I/O happens until `start()`. */
43
46
  constructor(image: string);
44
47
  /** Convenience: `new GenericContainer(image).start()`. */
45
48
  static start(image: string): Promise<GenericContainer>;
46
49
  /**
47
50
  * Builds a normal, ephemeral `GenericContainer` from a `Checkpoint`
48
- * (`checkpoint()`'s return value): image is `cp.imageRef`, and
51
+ * (`checkpoint()`'s return value): image is `cp.ref`, and
49
52
  * env/command/exposed ports/memory limit default to `cp.spec` — chain
50
53
  * further builder calls (a different `waitingFor`, `withBackend`, …)
51
54
  * before `start()`, the same as any other container. Never carries over
52
- * `cp.spec`'s network/aliases/mounts — the committed image already has
53
- * the filesystem baked in, and (as with `withReuse()`'s own network
55
+ * `cp.spec`'s network/aliases/mounts — the captured state already has the
56
+ * filesystem baked in, and (as with `withReuse()`'s own network
54
57
  * restriction) network topology is never part of what a checkpoint
55
- * captures. Once started, a restored container is ordinary in every
56
- * respect: fresh host ports, normal reaping-ledger tracking, normal stop.
58
+ * captures. `start()` throws `CheckpointBackendMismatchError` before any
59
+ * backend call if the active backend isn't the one that created `cp`, and
60
+ * `ReuseFromCheckpointError` if this container is also marked
61
+ * `withReuse()` — reuse's identity hash never covers a checkpoint ref.
62
+ * Once started, a restored container is ordinary in every other respect:
63
+ * fresh host ports, normal reaping-ledger tracking, normal stop.
57
64
  */
58
65
  static fromCheckpoint(cp: Checkpoint): GenericContainer;
59
66
  /** Sets an environment variable visible to the workload. Last-write-wins if called again with the same key; insertion order is otherwise preserved. */
@@ -222,19 +229,81 @@ export declare class GenericContainer implements AsyncDisposable, NetworkMember
222
229
  mappedPort(guestPort: number): number;
223
230
  private requireHandle;
224
231
  /**
225
- * Commits this running container's filesystem to a new image and returns
226
- * a `Checkpoint` — a FILESYSTEM capture, not a memory snapshot:
227
- * `fromCheckpoint()` boots a fresh container from the committed image
228
- * with processes restarting from scratch, not resuming. Requires the
229
- * active backend's `capabilities.checkpoint` (docker: `true`, via image
230
- * commit; microsandbox: `false`, no upstream microVM snapshot support
231
- * yet) — checked BEFORE any backend call, so an unsupported backend never
232
- * attempts one. Throws a state error (same shape as `exec`/`logs`) if
233
- * this container isn't currently running. Checkpoint images are never
234
- * auto-reaped (they're images, not containers) — see the checkpoints
235
- * guide for the manual cleanup one-liner.
236
- */
237
- checkpoint(): Promise<Checkpoint>;
232
+ * Captures this running container's state and returns a `Checkpoint` a
233
+ * FILESYSTEM capture, not a memory snapshot: `fromCheckpoint()` boots a
234
+ * container from the captured state with processes restarting from
235
+ * scratch, not resuming. Requires the active backend's
236
+ * `capabilities.checkpoint` (`true` on both real backends today: docker
237
+ * via image commit, microsandbox via disk snapshot) checked BEFORE any
238
+ * backend call, so an unsupported backend never attempts one. Throws a
239
+ * state error (same shape as `exec`/`logs`) if this container isn't
240
+ * currently running.
241
+ *
242
+ * Passing `name` makes this checkpoint NAMED and durable: the ref becomes
243
+ * deterministic (`rz-ckpt-<name>` / `rightsize/checkpoint:<name>` instead
244
+ * of a random 12-hex suffix), and — only once the backend call below has
245
+ * actually succeeded — a registry entry is written under
246
+ * `<cacheDir>/checkpoints/<name>.json` that `Checkpoints.find`/`list`/
247
+ * `remove` can rediscover later, in this process or a different one. `name`
248
+ * must match `^[a-z0-9][a-z0-9-]{0,40}$`; an invalid name throws
249
+ * `InvalidCheckpointNameError` before any backend call. Checkpointing under
250
+ * a name that already has a registry entry REPLACES it: the ref is the
251
+ * same deterministic value either way, so this best-effort clears the old
252
+ * artifact under that ref before creating the new one, then overwrites the
253
+ * registry entry — the latest checkpoint under a name always wins.
254
+ * Omitting `name` keeps the original behavior byte-for-byte: a random ref,
255
+ * no registry entry, ephemeral.
256
+ *
257
+ * On a backend whose `capabilities.checkpointRestartsWorkload` is `true`
258
+ * (microsandbox: the stop/snapshot/reboot cycle boots a fresh microVM),
259
+ * this re-runs the container's own wait strategy before returning — a bare
260
+ * return right after the backend call would hand back a false-ready
261
+ * container. docker's commit-to-image never disturbs the running
262
+ * container, so no re-wait happens there.
263
+ *
264
+ * That same reboot also kills any network links this container had
265
+ * installed toward already-running siblings (msb emulates them with
266
+ * exec-tunnels, which the workload restart tears down along with
267
+ * everything else on the sandbox), so when links were installed in the
268
+ * first place, this re-runs `installNetworkLinks` with those same links
269
+ * BEFORE the wait-strategy re-run — the wait must gate a container that is
270
+ * fully re-linked, not just booted.
271
+ *
272
+ * Checkpoints are never auto-reaped (a committed image or a disk snapshot
273
+ * is not a container) — see the checkpoints guide for the manual cleanup
274
+ * one-liners and the `Checkpoints.remove` cleanup affordance.
275
+ */
276
+ checkpoint(name?: string): Promise<Checkpoint>;
277
+ /**
278
+ * Copies a host file or directory into this running container at
279
+ * `containerPath`, creating the destination's parent directory first
280
+ * (`exec: mkdir -p <parent>`) — callers never pre-create directories on
281
+ * either side. `containerPath` must be absolute (both backends' own copy
282
+ * tools require `NAME:/abs/path`); `hostPath` may be a file or a
283
+ * directory, `cp -r`-style: copying a directory to an absent destination
284
+ * produces that destination as a copy of the source's CONTENTS, not the
285
+ * source nested one level down. Requires a running container — throws the
286
+ * same state error `exec()`/`logs()` do, before any backend call. Works on
287
+ * a reuse container too, but the copy mutates shared state that is NOT
288
+ * part of the reuse identity hash — see the reuse guide's caveat.
289
+ */
290
+ copyFileToContainer(hostPath: string, containerPath: string): Promise<void>;
291
+ /**
292
+ * Convenience over `copyFileToContainer()`: writes `content` to a private
293
+ * (mode `0600`) temp file and copies THAT file in, removing the temp file
294
+ * afterward regardless of outcome. No streaming protocol — content is
295
+ * always fully materialized on host disk before the copy.
296
+ */
297
+ copyContentToContainer(content: string | Uint8Array, containerPath: string): Promise<void>;
298
+ /**
299
+ * Copies a file or directory OUT of this running container at
300
+ * `containerPath` to `hostPath`, creating the destination's host parent
301
+ * directory first via the standard library — the mirror image of
302
+ * `copyFileToContainer`'s guest-side `mkdir -p`. `containerPath` must be
303
+ * absolute; `cp -r`-style destination naming applies here too. Requires a
304
+ * running container, like `copyFileToContainer`.
305
+ */
306
+ copyFileFromContainer(containerPath: string, hostPath: string): Promise<void>;
238
307
  /** Runs a one-shot command inside the running container and waits for it to exit. Throws if the container is not running. */
239
308
  exec(...cmd: string[]): Promise<ExecResult>;
240
309
  /** Fetches the workload's logs so far (a bounded tail), as a single string. Throws if the container is not running. */
@@ -1 +1 @@
1
- {"version":3,"file":"generic-container.d.ts","sourceRoot":"","sources":["../../src/core/generic-container.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD,OAAO,KAAK,EAAE,YAAY,EAAc,MAAM,WAAW,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAiB,YAAY,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAa,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACnF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAiEzD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,gBAAiB,YAAW,eAAe,EAAE,aAAa;IACrE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,YAAY,CAAyC;IAC7D,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,eAAe,CAA6B;IACpD,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,yBAAyB,CAAS;IAE1C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,WAAW,CAAkC;IACrD,OAAO,CAAC,OAAO,CAAS;IAExB,0FAA0F;gBAC9E,KAAK,EAAE,MAAM;IAIzB,0DAA0D;WAC7C,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAI5D;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,cAAc,CAAC,EAAE,EAAE,UAAU,GAAG,gBAAgB;IAiBvD,uJAAuJ;IACvJ,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IASzC,iHAAiH;IACjH,gBAAgB,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IAK1C,yGAAyG;IACzG,WAAW,CAAC,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI;IAKnC,sGAAsG;IACtG,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAK/B,qHAAqH;IACrH,kBAAkB,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IAK5C,uMAAuM;IACvM,uBAAuB,CAAC,IAAI,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAKrE,2GAA2G;IAC3G,UAAU,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI;IAKxC,8JAA8J;IAC9J,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAKxC,oFAAoF;IACpF,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAK1C;;;;;;;;;OASG;IACH,SAAS,IAAI,IAAI;IAKjB;;;;;;;OAOG;IACH,oBAAoB,IAAI,IAAI;IAK5B,6GAA6G;IAC7G,SAAS,CAAC,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,aAAa;IAI/F,mGAAmG;IACnG,SAAS,CAAC,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7C;;;;;;;;OAQG;IACH,SAAS,CAAC,mBAAmB,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE;;;;;;;OAOG;IACH,SAAS,CAAC,cAAc,IAAI,cAAc;IAI1C,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,SAAS;IAgCjB,uNAAuN;IACvN,OAAO,CAAC,cAAc;IA4BtB,kRAAkR;YACpQ,gBAAgB;YAWhB,aAAa;IAQ3B,OAAO,CAAC,YAAY;IAMpB;;;;;;;;;;;;;OAaG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAqI5B;;;;;;;OAOG;YACW,UAAU;IAoBxB;;;;;;;;;;OAUG;YACW,aAAa;IA6G3B;;;;;;;;OAQG;YACW,QAAQ;IA0BtB,uJAAuJ;YACzI,iBAAiB;IAK/B;;;;;;;;;;;;;;;;OAgBG;YACW,0BAA0B;IASxC,uPAAuP;IACvP,OAAO,CAAC,aAAa;IAgBrB;;;;;;OAMG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkC3B,oHAAoH;IAC9G,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5C,oEAAoE;IACpE,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,4FAA4F;IAC5F,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,2JAA2J;IAC3J,IAAI,OAAO,IAAI,aAAa,CAAC,MAAM,CAAC,CAEnC;IAED,oJAAoJ;IACpJ,IAAI,iBAAiB,IAAI,aAAa,CAAC,MAAM,CAAC,CAE7C;IAED;;;;;OAKG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAWxC,0HAA0H;IAC1H,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAIrC,OAAO,CAAC,aAAa;IAOrB;;;;;;;;;;;;OAYG;IACG,UAAU,IAAI,OAAO,CAAC,UAAU,CAAC;IAUvC,6HAA6H;IACvH,IAAI,CAAC,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC;IAKjD,uHAAuH;IACjH,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;IAK7B;;;;;OAKG;IACG,YAAY,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC;IAK3E,OAAO,CAAC,YAAY;CASrB"}
1
+ {"version":3,"file":"generic-container.d.ts","sourceRoot":"","sources":["../../src/core/generic-container.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,14 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
2
5
  import { FreePorts } from "./free-ports.js";
3
6
  import { RunId } from "./run-id.js";
4
7
  import { Network } from "./network.js";
5
- import { PortBindConflictError, IsolationRequiredError, CheckpointUnsupportedError } from "./errors.js";
8
+ import { PortBindConflictError, IsolationRequiredError, CheckpointUnsupportedError, CheckpointBackendMismatchError, ReuseFromCheckpointError, RelativeContainerPathError, BackendError, } from "./errors.js";
9
+ import { requireValidCheckpointName } from "./checkpoint/name.js";
10
+ import { checkpointRef } from "./checkpoint/ref.js";
11
+ import { toCheckpointRegistrySpec, writeCheckpointRegistryAtomic } from "./checkpoint/registry.js";
6
12
  import { Wait } from "./wait.js";
7
13
  import { registerSyncCleanup, unregisterSyncCleanup } from "./cleanup.js";
8
14
  import { Backends } from "./backends.js";
@@ -12,6 +18,12 @@ import { ReuseWithNetworkError } from "./errors.js";
12
18
  import { reuseEnabled } from "./reuse/env.js";
13
19
  import { reuseHash, reuseName } from "./reuse/hash.js";
14
20
  import { readRegistry, writeRegistryAtomic, removeRegistry } from "./reuse/registry.js";
21
+ /** Fails fast — before any backend call — on a relative `containerPath`: both backends require an absolute `NAME:/path` shape. */
22
+ function requireAbsoluteContainerPath(containerPath) {
23
+ if (!path.posix.isAbsolute(containerPath)) {
24
+ throw new RelativeContainerPathError(containerPath);
25
+ }
26
+ }
15
27
  const MAX_START_ATTEMPTS = 5;
16
28
  let sequence = 0;
17
29
  function nextSequence() {
@@ -94,10 +106,13 @@ export class GenericContainer {
94
106
  backendOverride;
95
107
  reuseRequested = false;
96
108
  requireIsolationRequested = false;
109
+ checkpointRef;
110
+ checkpointSourceBackend;
97
111
  handle;
98
112
  backend;
99
113
  mappedPorts = new Map();
100
114
  running = false;
115
+ installedNetworkLinks = [];
101
116
  /** Builds against `image` (e.g. `"redis:8.6-alpine"`); no I/O happens until `start()`. */
102
117
  constructor(image) {
103
118
  this.image = image;
@@ -108,18 +123,24 @@ export class GenericContainer {
108
123
  }
109
124
  /**
110
125
  * Builds a normal, ephemeral `GenericContainer` from a `Checkpoint`
111
- * (`checkpoint()`'s return value): image is `cp.imageRef`, and
126
+ * (`checkpoint()`'s return value): image is `cp.ref`, and
112
127
  * env/command/exposed ports/memory limit default to `cp.spec` — chain
113
128
  * further builder calls (a different `waitingFor`, `withBackend`, …)
114
129
  * before `start()`, the same as any other container. Never carries over
115
- * `cp.spec`'s network/aliases/mounts — the committed image already has
116
- * the filesystem baked in, and (as with `withReuse()`'s own network
130
+ * `cp.spec`'s network/aliases/mounts — the captured state already has the
131
+ * filesystem baked in, and (as with `withReuse()`'s own network
117
132
  * restriction) network topology is never part of what a checkpoint
118
- * captures. Once started, a restored container is ordinary in every
119
- * respect: fresh host ports, normal reaping-ledger tracking, normal stop.
133
+ * captures. `start()` throws `CheckpointBackendMismatchError` before any
134
+ * backend call if the active backend isn't the one that created `cp`, and
135
+ * `ReuseFromCheckpointError` if this container is also marked
136
+ * `withReuse()` — reuse's identity hash never covers a checkpoint ref.
137
+ * Once started, a restored container is ordinary in every other respect:
138
+ * fresh host ports, normal reaping-ledger tracking, normal stop.
120
139
  */
121
140
  static fromCheckpoint(cp) {
122
- const container = new GenericContainer(cp.imageRef);
141
+ const container = new GenericContainer(cp.ref);
142
+ container.checkpointRef = cp.ref;
143
+ container.checkpointSourceBackend = cp.backend;
123
144
  for (const [key, value] of cp.spec.env) {
124
145
  container.withEnv(key, value);
125
146
  }
@@ -265,6 +286,7 @@ export class GenericContainer {
265
286
  // requested-but-env-disabled reuse container falls through to here
266
287
  // deliberately (Testcontainers semantics), so it must stay false.
267
288
  keepAlive: false,
289
+ checkpointRef: this.checkpointRef,
268
290
  };
269
291
  return this.customizeSpec(spec, (guest) => {
270
292
  const p = ports.get(guest);
@@ -293,6 +315,10 @@ export class GenericContainer {
293
315
  runId: RunId.value,
294
316
  memoryLimitMb: this.memoryLimitMb,
295
317
  keepAlive: true,
318
+ // Reuse + fromCheckpoint is rejected in start() before this is ever
319
+ // built (see ReuseFromCheckpointError) — always undefined in
320
+ // practice, present only to satisfy ContainerSpec's shape.
321
+ checkpointRef: this.checkpointRef,
296
322
  };
297
323
  return this.customizeSpec(spec, (guest) => {
298
324
  const p = ports.get(guest);
@@ -341,6 +367,12 @@ export class GenericContainer {
341
367
  */
342
368
  async start() {
343
369
  const backend = this.resolveBackend();
370
+ // Before any backend work at all: a checkpoint ref is only meaningful
371
+ // to the backend that minted it (a docker image tag means nothing to
372
+ // msb, a msb snapshot name means nothing to docker).
373
+ if (this.checkpointSourceBackend !== undefined && this.checkpointSourceBackend !== backend.name) {
374
+ throw new CheckpointBackendMismatchError(this.checkpointSourceBackend, backend.name);
375
+ }
344
376
  // Only the env-resolved path (never an explicit withBackend() override)
345
377
  // drives the reaper: writes this process's run record, sweeps dead runs
346
378
  // on first use, and spawns the watchdog. Skipping it for overrides is
@@ -367,6 +399,9 @@ export class GenericContainer {
367
399
  if (this.network !== undefined) {
368
400
  throw new ReuseWithNetworkError();
369
401
  }
402
+ if (this.checkpointRef !== undefined) {
403
+ throw new ReuseFromCheckpointError();
404
+ }
370
405
  return this.startReuse(backend);
371
406
  }
372
407
  process.stderr.write(`[rightsize] reuse was requested via withReuse() for '${this.image}' but RIGHTSIZE_REUSE is not ` +
@@ -441,6 +476,7 @@ export class GenericContainer {
441
476
  try {
442
477
  const links = this.network?.linksForNewMember() ?? [];
443
478
  await backend.installNetworkLinks(handle, links);
479
+ this.installedNetworkLinks = links;
444
480
  // Register AFTER links are computed/installed — a container must
445
481
  // never see itself in its own linksForNewMember() call.
446
482
  if (this.network !== undefined) {
@@ -745,26 +781,139 @@ export class GenericContainer {
745
781
  return { handle: this.handle, backend: this.backend };
746
782
  }
747
783
  /**
748
- * Commits this running container's filesystem to a new image and returns
749
- * a `Checkpoint` — a FILESYSTEM capture, not a memory snapshot:
750
- * `fromCheckpoint()` boots a fresh container from the committed image
751
- * with processes restarting from scratch, not resuming. Requires the
752
- * active backend's `capabilities.checkpoint` (docker: `true`, via image
753
- * commit; microsandbox: `false`, no upstream microVM snapshot support
754
- * yet) — checked BEFORE any backend call, so an unsupported backend never
755
- * attempts one. Throws a state error (same shape as `exec`/`logs`) if
756
- * this container isn't currently running. Checkpoint images are never
757
- * auto-reaped (they're images, not containers) — see the checkpoints
758
- * guide for the manual cleanup one-liner.
784
+ * Captures this running container's state and returns a `Checkpoint` a
785
+ * FILESYSTEM capture, not a memory snapshot: `fromCheckpoint()` boots a
786
+ * container from the captured state with processes restarting from
787
+ * scratch, not resuming. Requires the active backend's
788
+ * `capabilities.checkpoint` (`true` on both real backends today: docker
789
+ * via image commit, microsandbox via disk snapshot) checked BEFORE any
790
+ * backend call, so an unsupported backend never attempts one. Throws a
791
+ * state error (same shape as `exec`/`logs`) if this container isn't
792
+ * currently running.
793
+ *
794
+ * Passing `name` makes this checkpoint NAMED and durable: the ref becomes
795
+ * deterministic (`rz-ckpt-<name>` / `rightsize/checkpoint:<name>` instead
796
+ * of a random 12-hex suffix), and — only once the backend call below has
797
+ * actually succeeded — a registry entry is written under
798
+ * `<cacheDir>/checkpoints/<name>.json` that `Checkpoints.find`/`list`/
799
+ * `remove` can rediscover later, in this process or a different one. `name`
800
+ * must match `^[a-z0-9][a-z0-9-]{0,40}$`; an invalid name throws
801
+ * `InvalidCheckpointNameError` before any backend call. Checkpointing under
802
+ * a name that already has a registry entry REPLACES it: the ref is the
803
+ * same deterministic value either way, so this best-effort clears the old
804
+ * artifact under that ref before creating the new one, then overwrites the
805
+ * registry entry — the latest checkpoint under a name always wins.
806
+ * Omitting `name` keeps the original behavior byte-for-byte: a random ref,
807
+ * no registry entry, ephemeral.
808
+ *
809
+ * On a backend whose `capabilities.checkpointRestartsWorkload` is `true`
810
+ * (microsandbox: the stop/snapshot/reboot cycle boots a fresh microVM),
811
+ * this re-runs the container's own wait strategy before returning — a bare
812
+ * return right after the backend call would hand back a false-ready
813
+ * container. docker's commit-to-image never disturbs the running
814
+ * container, so no re-wait happens there.
815
+ *
816
+ * That same reboot also kills any network links this container had
817
+ * installed toward already-running siblings (msb emulates them with
818
+ * exec-tunnels, which the workload restart tears down along with
819
+ * everything else on the sandbox), so when links were installed in the
820
+ * first place, this re-runs `installNetworkLinks` with those same links
821
+ * BEFORE the wait-strategy re-run — the wait must gate a container that is
822
+ * fully re-linked, not just booted.
823
+ *
824
+ * Checkpoints are never auto-reaped (a committed image or a disk snapshot
825
+ * is not a container) — see the checkpoints guide for the manual cleanup
826
+ * one-liners and the `Checkpoints.remove` cleanup affordance.
759
827
  */
760
- async checkpoint() {
828
+ async checkpoint(name) {
829
+ if (name !== undefined) {
830
+ requireValidCheckpointName(name);
831
+ }
761
832
  const { handle, backend } = this.requireHandle();
762
833
  if (!backend.capabilities.checkpoint) {
763
834
  throw new CheckpointUnsupportedError(backend.name);
764
835
  }
765
- const imageRef = `rightsize/checkpoint:${randomBytes(6).toString("hex")}`;
766
- await backend.commitToImage(handle, imageRef);
767
- return { imageRef, spec: handle.spec };
836
+ const ref = checkpointRef(backend.name, name);
837
+ if (name !== undefined) {
838
+ // Replace semantics: the ref is deterministic from `name`, so a prior
839
+ // checkpoint under this same name — if any — sits under this exact
840
+ // ref. Best-effort clear it before creating the new one.
841
+ await swallow(() => backend.removeCheckpoint(ref));
842
+ }
843
+ await backend.createCheckpoint(handle, ref);
844
+ if (backend.capabilities.checkpointRestartsWorkload) {
845
+ if (this.installedNetworkLinks.length > 0) {
846
+ await backend.installNetworkLinks(handle, this.installedNetworkLinks);
847
+ }
848
+ await this.waitStrategy.waitUntilReady(this.asWaitTarget());
849
+ }
850
+ if (name !== undefined) {
851
+ // Only after the backend checkpoint above has actually succeeded — a
852
+ // failed createCheckpoint() already threw, so a registry entry is
853
+ // never written for a checkpoint that doesn't exist.
854
+ const entry = {
855
+ name,
856
+ ref,
857
+ backend: backend.name,
858
+ createdIso: new Date().toISOString(),
859
+ spec: toCheckpointRegistrySpec(handle.spec),
860
+ };
861
+ await writeCheckpointRegistryAtomic(cacheDir(), name, entry);
862
+ }
863
+ return { ref, backend: backend.name, spec: handle.spec };
864
+ }
865
+ /**
866
+ * Copies a host file or directory into this running container at
867
+ * `containerPath`, creating the destination's parent directory first
868
+ * (`exec: mkdir -p <parent>`) — callers never pre-create directories on
869
+ * either side. `containerPath` must be absolute (both backends' own copy
870
+ * tools require `NAME:/abs/path`); `hostPath` may be a file or a
871
+ * directory, `cp -r`-style: copying a directory to an absent destination
872
+ * produces that destination as a copy of the source's CONTENTS, not the
873
+ * source nested one level down. Requires a running container — throws the
874
+ * same state error `exec()`/`logs()` do, before any backend call. Works on
875
+ * a reuse container too, but the copy mutates shared state that is NOT
876
+ * part of the reuse identity hash — see the reuse guide's caveat.
877
+ */
878
+ async copyFileToContainer(hostPath, containerPath) {
879
+ const { handle, backend } = this.requireHandle();
880
+ requireAbsoluteContainerPath(containerPath);
881
+ const parent = path.posix.dirname(containerPath);
882
+ const mkdir = await backend.exec(handle, ["mkdir", "-p", parent]);
883
+ if (mkdir.exitCode !== 0) {
884
+ throw new BackendError(`could not create parent directory '${parent}' in '${handle.spec.name}' before copying in: ${mkdir.stderr.trim()}`);
885
+ }
886
+ await backend.copyToContainer(handle, hostPath, containerPath);
887
+ }
888
+ /**
889
+ * Convenience over `copyFileToContainer()`: writes `content` to a private
890
+ * (mode `0600`) temp file and copies THAT file in, removing the temp file
891
+ * afterward regardless of outcome. No streaming protocol — content is
892
+ * always fully materialized on host disk before the copy.
893
+ */
894
+ async copyContentToContainer(content, containerPath) {
895
+ const tempPath = path.join(os.tmpdir(), `rightsize-copy-content-${randomBytes(6).toString("hex")}`);
896
+ await fs.writeFile(tempPath, content, { mode: 0o600 });
897
+ try {
898
+ await this.copyFileToContainer(tempPath, containerPath);
899
+ }
900
+ finally {
901
+ await fs.rm(tempPath, { force: true });
902
+ }
903
+ }
904
+ /**
905
+ * Copies a file or directory OUT of this running container at
906
+ * `containerPath` to `hostPath`, creating the destination's host parent
907
+ * directory first via the standard library — the mirror image of
908
+ * `copyFileToContainer`'s guest-side `mkdir -p`. `containerPath` must be
909
+ * absolute; `cp -r`-style destination naming applies here too. Requires a
910
+ * running container, like `copyFileToContainer`.
911
+ */
912
+ async copyFileFromContainer(containerPath, hostPath) {
913
+ const { handle, backend } = this.requireHandle();
914
+ requireAbsoluteContainerPath(containerPath);
915
+ await fs.mkdir(path.dirname(hostPath), { recursive: true });
916
+ await backend.copyFromContainer(handle, containerPath, hostPath);
768
917
  }
769
918
  /** Runs a one-shot command inside the running container and waits for it to exit. Throws if the container is not running. */
770
919
  async exec(...cmd) {