@mastra/docker 0.9.0-alpha.1 → 0.9.0-alpha.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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { DockerSandbox, type DockerSandboxOptions, type DockerTemplateSpec } from './sandbox/index.js';
1
+ export { DockerSandbox, type DockerSandboxOptions, type DockerSandboxStartOptions, type DockerTemplateSpec, } from './sandbox/index.js';
2
2
  export { DockerProcessManager } from './sandbox/process-manager.js';
3
3
  export { dockerSandboxProvider } from './provider.js';
4
4
  export * from './template/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC9F,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,cAAc,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -7,6 +7,38 @@ import { createHash, randomUUID } from "crypto";
7
7
  import posixPath from "path/posix";
8
8
  import { Server, ServerCredentials } from "@grpc/grpc-js";
9
9
  import { execFile } from "child_process";
10
+ //#region src/abort.ts
11
+ function createAbortError(signal, operation) {
12
+ return new SandboxAbortError(operation, signal.reason);
13
+ }
14
+ function throwIfAborted(signal, operation) {
15
+ if (signal?.aborted) throw createAbortError(signal, operation);
16
+ }
17
+ function isAbortError(error) {
18
+ return error instanceof SandboxAbortError || error instanceof Error && (error.name === "AbortError" || "code" in error && error.code === "ABORT_ERR");
19
+ }
20
+ function normalizeAbortError(error, operation) {
21
+ return error instanceof SandboxAbortError ? error : isAbortError(error) ? new SandboxAbortError(operation, error) : error;
22
+ }
23
+ function waitForAbortable(promise, signal, operation, onAbort) {
24
+ throwIfAborted(signal, operation);
25
+ if (!signal) return promise;
26
+ return new Promise((resolve, reject) => {
27
+ const abort = () => {
28
+ onAbort?.();
29
+ reject(new SandboxAbortError(operation, signal.reason));
30
+ };
31
+ signal.addEventListener("abort", abort, { once: true });
32
+ promise.then((value) => {
33
+ signal.removeEventListener("abort", abort);
34
+ resolve(value);
35
+ }, (error) => {
36
+ signal.removeEventListener("abort", abort);
37
+ reject(normalizeAbortError(error, operation));
38
+ });
39
+ });
40
+ }
41
+ //#endregion
10
42
  //#region src/sandbox/process-manager.ts
11
43
  /**
12
44
  * Docker Process Manager
@@ -544,7 +576,9 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
544
576
  if (!this._container) throw new SandboxNotReadyError(this.id);
545
577
  return this._container;
546
578
  }
547
- async start() {
579
+ async start(options = {}) {
580
+ const { abortSignal } = options;
581
+ throwIfAborted(abortSignal, "start Docker sandbox");
548
582
  this.logger.debug(`${LOG_PREFIX} Starting sandbox ${this.id}...`);
549
583
  const existing = await this._findExistingContainer();
550
584
  if (existing) {
@@ -565,7 +599,7 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
565
599
  return;
566
600
  }
567
601
  this._warnOnPrivilegedHardeningConflict(this._privileged);
568
- await this._resolveTemplate();
602
+ await this._resolveTemplate(abortSignal);
569
603
  await this._ensureImage();
570
604
  const envArray = Object.entries(this._env).map(([k, v]) => `${k}=${v}`);
571
605
  const binds = Object.entries(this._volumes).map(([host, container]) => `${host}:${container}`);
@@ -825,10 +859,20 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
825
859
  * creates a container, so a resolver-form template re-resolves each time.
826
860
  * Adopts the template's workdir unless the sandbox was given one explicitly.
827
861
  */
828
- async _resolveTemplate() {
862
+ async _resolveTemplate(abortSignal) {
829
863
  if (!this._templateSpec) return;
830
- const template = typeof this._templateSpec === "function" ? await this._templateSpec() : this._templateSpec;
831
- const result = await template.build({ docker: this._docker });
864
+ throwIfAborted(abortSignal, "start Docker sandbox");
865
+ let template;
866
+ try {
867
+ template = typeof this._templateSpec === "function" ? await this._templateSpec({ abortSignal }) : this._templateSpec;
868
+ } catch (error) {
869
+ throw normalizeAbortError(error, "start Docker sandbox");
870
+ }
871
+ throwIfAborted(abortSignal, "start Docker sandbox");
872
+ const result = await template.build({
873
+ docker: this._docker,
874
+ abortSignal
875
+ });
832
876
  if (result.status !== "ready") throw new SandboxError(`Docker template build failed: ${result.error ?? "unknown error"}`, "START_FAILED", {
833
877
  templateId: result.templateId,
834
878
  reason: "template_build_failed"
@@ -1201,9 +1245,17 @@ const AUTH_CREDENTIALS_METHOD = "/moby.filesync.v1.Auth/Credentials";
1201
1245
  * Open a session that answers `GetSecret` from `secrets`. The returned id must
1202
1246
  * be sent as the `session` query parameter of the build request.
1203
1247
  */
1204
- function openBuildSession(docker, secrets) {
1248
+ function openBuildSession(docker, secrets, abortSignal) {
1249
+ throwIfAborted(abortSignal, "build Docker template");
1205
1250
  const id = randomUUID();
1206
1251
  return new Promise((resolve, reject) => {
1252
+ let settled = false;
1253
+ const abort = () => {
1254
+ if (settled || !abortSignal) return;
1255
+ settled = true;
1256
+ reject(createAbortError(abortSignal, "build Docker template"));
1257
+ };
1258
+ abortSignal?.addEventListener("abort", abort, { once: true });
1207
1259
  docker.modem.dial({
1208
1260
  method: "POST",
1209
1261
  path: "/session",
@@ -1219,8 +1271,14 @@ function openBuildSession(docker, secrets) {
1219
1271
  500: "server error"
1220
1272
  }
1221
1273
  }, (err, socket) => {
1274
+ if (settled) {
1275
+ socket?.end?.();
1276
+ return;
1277
+ }
1278
+ settled = true;
1279
+ abortSignal?.removeEventListener("abort", abort);
1222
1280
  if (err) {
1223
- reject(err);
1281
+ reject(abortSignal?.aborted ? createAbortError(abortSignal, "build Docker template") : err);
1224
1282
  return;
1225
1283
  }
1226
1284
  const server = new Server();
@@ -1541,7 +1599,8 @@ var DockerTemplate = class DockerTemplate {
1541
1599
  #secrets;
1542
1600
  #docker;
1543
1601
  #built = false;
1544
- #inFlight;
1602
+ #queueTail;
1603
+ #plainBuild;
1545
1604
  constructor(options = {}, state) {
1546
1605
  this.#baseImage = state ? state.baseImage : validateString(options.baseImage ?? "node:22-slim", "baseImage");
1547
1606
  this.#operations = state?.operations ?? [];
@@ -1688,19 +1747,49 @@ var DockerTemplate = class DockerTemplate {
1688
1747
  * `options.secrets`, the template's `secrets` option, or `process.env`.
1689
1748
  */
1690
1749
  async build(options = {}) {
1691
- if (!options.force && options.secrets === void 0 && options.docker === void 0 && this.#inFlight) return this.#inFlight;
1692
- const run = (this.#inFlight?.catch(() => void 0) ?? Promise.resolve()).then(() => this.#build(options));
1693
- this.#inFlight = run;
1694
- run.finally(() => {
1695
- if (this.#inFlight === run) this.#inFlight = void 0;
1750
+ throwIfAborted(options.abortSignal, "build Docker template");
1751
+ const isPlain = !options.force && options.secrets === void 0 && options.docker === void 0;
1752
+ if (isPlain && this.#plainBuild) return this.#waitForBuild(this.#plainBuild, options.abortSignal);
1753
+ const previous = this.#queueTail?.promise.catch(() => void 0) ?? Promise.resolve();
1754
+ const controller = new AbortController();
1755
+ const run = {
1756
+ controller,
1757
+ waiters: 0,
1758
+ promise: previous.then(() => {
1759
+ throwIfAborted(controller.signal, "build Docker template");
1760
+ return this.#build({
1761
+ ...options,
1762
+ abortSignal: controller.signal
1763
+ });
1764
+ })
1765
+ };
1766
+ this.#queueTail = run;
1767
+ if (isPlain) this.#plainBuild = run;
1768
+ run.promise.finally(() => {
1769
+ if (this.#queueTail === run) this.#queueTail = void 0;
1770
+ if (this.#plainBuild === run) this.#plainBuild = void 0;
1696
1771
  }).catch(() => void 0);
1697
- return run;
1772
+ return this.#waitForBuild(run, options.abortSignal);
1773
+ }
1774
+ #waitForBuild(run, abortSignal) {
1775
+ run.waiters += 1;
1776
+ let settled = false;
1777
+ const release = (aborted) => {
1778
+ if (settled) return;
1779
+ settled = true;
1780
+ run.waiters -= 1;
1781
+ if (aborted && run.waiters === 0) run.controller.abort();
1782
+ };
1783
+ return waitForAbortable(run.promise, abortSignal, "build Docker template", () => release(true)).finally(() => release(false));
1698
1784
  }
1699
1785
  async #build(options) {
1700
1786
  const docker = options.docker ?? this.#getDocker();
1701
1787
  const tag = this.templateId;
1788
+ const { abortSignal } = options;
1789
+ throwIfAborted(abortSignal, "build Docker template");
1702
1790
  if (!options.force) try {
1703
1791
  await docker.getImage(tag).inspect();
1792
+ throwIfAborted(abortSignal, "build Docker template");
1704
1793
  this.#built = true;
1705
1794
  return {
1706
1795
  status: "ready",
@@ -1710,24 +1799,37 @@ var DockerTemplate = class DockerTemplate {
1710
1799
  if (!isImageNotFoundError(error)) throw error;
1711
1800
  }
1712
1801
  const secrets = await this.#resolveSecrets(options.secrets);
1802
+ throwIfAborted(abortSignal, "build Docker template");
1713
1803
  const context = pack();
1714
1804
  context.entry({ name: "Dockerfile" }, this.dockerfile);
1715
1805
  context.finalize();
1716
1806
  try {
1717
1807
  const nocache = options.force === true;
1718
1808
  if (secrets) {
1719
- const { stream, session } = await this.#buildWithSecrets(docker, context, tag, secrets, nocache);
1809
+ const { stream, session } = await this.#buildWithSecrets(docker, context, tag, secrets, nocache, abortSignal);
1720
1810
  try {
1721
- await this.#followBuild(docker, stream);
1811
+ await this.#followBuild(docker, stream, abortSignal);
1722
1812
  } finally {
1723
1813
  session.close();
1724
1814
  }
1725
- } else await this.#followBuild(docker, await docker.buildImage(context, {
1726
- t: tag,
1727
- nocache
1728
- }));
1815
+ } else {
1816
+ const stream = await waitForAbortable(docker.buildImage(context, {
1817
+ t: tag,
1818
+ nocache
1819
+ }).then((stream) => {
1820
+ if (abortSignal?.aborted) {
1821
+ stream.destroy?.();
1822
+ throw createAbortError(abortSignal, "build Docker template");
1823
+ }
1824
+ return stream;
1825
+ }), abortSignal, "build Docker template", () => {
1826
+ context.destroy?.();
1827
+ });
1828
+ await this.#followBuild(docker, stream, abortSignal);
1829
+ }
1729
1830
  } catch (error) {
1730
1831
  this.#built = false;
1832
+ if (abortSignal?.aborted) throw normalizeAbortError(error, "build Docker template");
1731
1833
  return {
1732
1834
  status: "failed",
1733
1835
  templateId: tag,
@@ -1758,12 +1860,35 @@ var DockerTemplate = class DockerTemplate {
1758
1860
  * directly because `docker.buildImage` replaces any session id with its own
1759
1861
  * auth-only session when `version` is `'2'`.
1760
1862
  */
1761
- async #buildWithSecrets(docker, context, tag, secrets, nocache) {
1762
- const session = await openBuildSession(docker, secrets);
1863
+ async #buildWithSecrets(docker, context, tag, secrets, nocache, abortSignal) {
1864
+ throwIfAborted(abortSignal, "build Docker template");
1865
+ const session = await openBuildSession(docker, secrets, abortSignal);
1763
1866
  let stream;
1764
1867
  try {
1765
1868
  stream = await new Promise((resolve, reject) => {
1766
- docker.modem.dial({
1869
+ let settled = false;
1870
+ let request;
1871
+ const finish = (error, data) => {
1872
+ if (settled) {
1873
+ if (data) data.destroy?.();
1874
+ return;
1875
+ }
1876
+ settled = true;
1877
+ abortSignal?.removeEventListener("abort", abort);
1878
+ error === void 0 ? resolve(data) : reject(error);
1879
+ };
1880
+ const abort = () => {
1881
+ context.destroy?.();
1882
+ request?.destroy?.();
1883
+ session.close();
1884
+ if (abortSignal) finish(createAbortError(abortSignal, "build Docker template"));
1885
+ };
1886
+ abortSignal?.addEventListener("abort", abort, { once: true });
1887
+ if (abortSignal?.aborted) {
1888
+ abort();
1889
+ return;
1890
+ }
1891
+ request = docker.modem.dial({
1767
1892
  path: "/build?",
1768
1893
  method: "POST",
1769
1894
  file: context,
@@ -1778,7 +1903,7 @@ var DockerTemplate = class DockerTemplate {
1778
1903
  200: true,
1779
1904
  500: "server error"
1780
1905
  }
1781
- }, (err, data) => err ? reject(err) : resolve(data));
1906
+ }, (err, data) => finish(err ?? void 0, data));
1782
1907
  });
1783
1908
  } catch (error) {
1784
1909
  session.close();
@@ -1789,20 +1914,36 @@ var DockerTemplate = class DockerTemplate {
1789
1914
  session
1790
1915
  };
1791
1916
  }
1792
- #followBuild(docker, stream) {
1917
+ #followBuild(docker, stream, abortSignal) {
1793
1918
  return new Promise((resolve, reject) => {
1919
+ let settled = false;
1920
+ const finish = (error) => {
1921
+ if (settled) return;
1922
+ settled = true;
1923
+ abortSignal?.removeEventListener("abort", abort);
1924
+ error === void 0 ? resolve() : reject(error);
1925
+ };
1926
+ const abort = () => {
1927
+ stream.destroy?.();
1928
+ if (abortSignal) finish(createAbortError(abortSignal, "build Docker template"));
1929
+ };
1930
+ abortSignal?.addEventListener("abort", abort, { once: true });
1931
+ if (abortSignal?.aborted) {
1932
+ abort();
1933
+ return;
1934
+ }
1794
1935
  docker.modem.followProgress(stream, (err, output) => {
1795
1936
  if (err) {
1796
- reject(err);
1937
+ finish(err);
1797
1938
  return;
1798
1939
  }
1799
1940
  const failure = output?.find((entry) => entry && (entry.error !== void 0 || entry.errorDetail !== void 0));
1800
1941
  if (failure) {
1801
1942
  const detail = failure.errorDetail;
1802
- reject(new Error(String(detail?.message ?? failure.error ?? "docker build failed")));
1943
+ finish(new Error(String(detail?.message ?? failure.error ?? "docker build failed")));
1803
1944
  return;
1804
1945
  }
1805
- resolve();
1946
+ finish();
1806
1947
  });
1807
1948
  });
1808
1949
  }
@@ -1951,16 +2092,31 @@ function createDockerRepoTemplate(options) {
1951
2092
  if (options.ref !== void 0 && !REF_PATTERN.test(options.ref)) throw new Error(`Invalid ref '${options.ref}': expected a git ref name`);
1952
2093
  const workingDirectory = trimTrailingSlashes(options.workingDirectory ?? DEFAULT_WORKING_DIRECTORY);
1953
2094
  if (!workingDirectory.startsWith("/")) throw new Error(`workingDirectory must be an absolute path, got '${options.workingDirectory}'`);
1954
- return () => resolveRepoTemplate(options, workingDirectory);
2095
+ return (resolveOptions) => resolveRepoTemplate(options, workingDirectory, resolveOptions);
1955
2096
  }
1956
- async function resolveRepoTemplate(options, workingDirectory) {
1957
- const access = await options.getRepositoryAccess();
2097
+ async function resolveRepoTemplate(options, workingDirectory, resolveOptions = {}) {
2098
+ const { abortSignal } = resolveOptions;
2099
+ throwIfAborted(abortSignal, "resolve Docker repository template");
2100
+ let access;
2101
+ try {
2102
+ access = await options.getRepositoryAccess(resolveOptions);
2103
+ } catch (error) {
2104
+ throw normalizeAbortError(error, "resolve Docker repository template");
2105
+ }
2106
+ throwIfAborted(abortSignal, "resolve Docker repository template");
1958
2107
  const cloneUrl = access?.cloneUrl;
1959
2108
  if (!cloneUrl) throw new Error("Repo template has no clone URL: repository access returned none.");
1960
2109
  assertCloneUrl(cloneUrl);
1961
2110
  const token = access?.authorization?.token;
1962
- const buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv() : options.buildEnv;
1963
- const sha = await resolveHead(cloneUrl, options.ref, token);
2111
+ let buildEnv;
2112
+ try {
2113
+ buildEnv = typeof options.buildEnv === "function" ? await options.buildEnv(resolveOptions) : options.buildEnv;
2114
+ } catch (error) {
2115
+ throw normalizeAbortError(error, "resolve Docker repository template");
2116
+ }
2117
+ throwIfAborted(abortSignal, "resolve Docker repository template");
2118
+ const sha = await resolveHead(cloneUrl, options.ref, token, abortSignal);
2119
+ throwIfAborted(abortSignal, "resolve Docker repository template");
1964
2120
  if (!sha) throw new Error(`Could not resolve ${options.ref ?? "HEAD"} of ${cloneUrl} with git ls-remote; check the ref, the credential and network access`);
1965
2121
  return buildRepoTemplate({
1966
2122
  cloneUrl,
@@ -2009,7 +2165,8 @@ function buildRepoTemplate(inputs) {
2009
2165
  * yields undefined; the caller decides how to surface it.
2010
2166
  * @internal exported for tests.
2011
2167
  */
2012
- async function resolveHead(cloneUrl, ref, token) {
2168
+ async function resolveHead(cloneUrl, ref, token, abortSignal) {
2169
+ throwIfAborted(abortSignal, "resolve Docker repository template");
2013
2170
  if (ref && FULL_SHA_PATTERN.test(ref)) return ref.toLowerCase();
2014
2171
  try {
2015
2172
  const authEnv = token ? {
@@ -2024,6 +2181,7 @@ async function resolveHead(cloneUrl, ref, token) {
2024
2181
  ref ?? "HEAD"
2025
2182
  ], {
2026
2183
  timeout: 1e4,
2184
+ signal: abortSignal,
2027
2185
  env: {
2028
2186
  ...process.env,
2029
2187
  ...authEnv,
@@ -2033,7 +2191,8 @@ async function resolveHead(cloneUrl, ref, token) {
2033
2191
  const lines = stdout.split("\n").filter(Boolean).map((line) => line.split(" "));
2034
2192
  const sha = (lines.find(([, name]) => name?.endsWith("^{}")) ?? lines[0])?.[0]?.trim();
2035
2193
  return sha && SHA_PATTERN.test(sha) ? sha.toLowerCase() : void 0;
2036
- } catch {
2194
+ } catch (error) {
2195
+ if (abortSignal?.aborted) throw normalizeAbortError(error, "resolve Docker repository template");
2037
2196
  return;
2038
2197
  }
2039
2198
  }