@mastra/docker 0.8.0-alpha.0 → 0.8.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/README.md +33 -0
- package/dist/index.cjs +281 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +282 -34
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +2 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/sandbox/index.d.ts +100 -4
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/process-manager.d.ts.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -29,6 +29,39 @@ const agent = new Agent({
|
|
|
29
29
|
});
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
### Volume subpath mounts
|
|
33
|
+
|
|
34
|
+
Use `mounts` (mapped 1:1 onto Docker's `HostConfig.Mounts`) when you need mount
|
|
35
|
+
options that the `-v`/`volumes` syntax cannot express — most notably mounting a
|
|
36
|
+
subdirectory of a named volume. Requires Docker Engine 26.0+ (API v1.45+) for
|
|
37
|
+
`subpath`.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
const workspace = new Workspace({
|
|
41
|
+
sandbox: new DockerSandbox({
|
|
42
|
+
image: 'node:22-slim',
|
|
43
|
+
mounts: [
|
|
44
|
+
// Read-only parent from a named volume
|
|
45
|
+
{ type: 'volume', source: 'project-data', target: '/shared', readOnly: true },
|
|
46
|
+
// Writable per-conversation subdirectory of the same volume
|
|
47
|
+
{
|
|
48
|
+
type: 'volume',
|
|
49
|
+
source: 'project-data',
|
|
50
|
+
target: '/work',
|
|
51
|
+
volumeOptions: { subpath: 'conversations/abc123' },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`volumes` and `mounts` can be combined; both are passed through to Docker.
|
|
59
|
+
|
|
60
|
+
> **Note:** Docker does not create `volumeOptions.subpath` for you — the
|
|
61
|
+
> subdirectory must already exist inside the named volume before the container
|
|
62
|
+
> starts, otherwise the mount fails. Provision it ahead of time (for example,
|
|
63
|
+
> with a one-off container that creates `conversations/abc123` in the volume).
|
|
64
|
+
|
|
32
65
|
## Documentation
|
|
33
66
|
|
|
34
67
|
- [Docker Sandbox integration guide](https://mastra.ai/integrations/sandboxes/docker)
|
package/dist/index.cjs
CHANGED
|
@@ -27,8 +27,109 @@ let _mastra_core_workspace = require("@mastra/core/workspace");
|
|
|
27
27
|
let dockerode = require("dockerode");
|
|
28
28
|
dockerode = __toESM(dockerode, 1);
|
|
29
29
|
let tar_stream = require("tar-stream");
|
|
30
|
+
let crypto = require("crypto");
|
|
30
31
|
//#region src/sandbox/process-manager.ts
|
|
31
32
|
/**
|
|
33
|
+
* Docker Process Manager
|
|
34
|
+
*
|
|
35
|
+
* Implements SandboxProcessManager for Docker containers.
|
|
36
|
+
* Uses `container.exec()` to run commands inside a long-lived container.
|
|
37
|
+
* Each spawned process gets a dedicated exec instance with separate
|
|
38
|
+
* stdout/stderr streams.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Directory (inside the container) where each spawned process records the PGID
|
|
42
|
+
* of its process group. Created with mode 700 so only the (root) exec user can
|
|
43
|
+
* write the PGID files.
|
|
44
|
+
*/
|
|
45
|
+
const PROC_DIR = "/tmp/.mastra-proc";
|
|
46
|
+
/**
|
|
47
|
+
* Wrapper (run as the exec command) that places the user command in its own
|
|
48
|
+
* process group and records the group's PGID so kill() can signal the whole
|
|
49
|
+
* group later.
|
|
50
|
+
*
|
|
51
|
+
* Docker's exec-inspect `Pid` is a host/daemon-namespace PID and cannot be used
|
|
52
|
+
* with an in-container `kill`, so we need a container-namespace identity. We use
|
|
53
|
+
* a *kernel-enforced* process group as that identity:
|
|
54
|
+
*
|
|
55
|
+
* 1. `setsid -w` re-execs the command as a new session/process-group leader,
|
|
56
|
+
* so its PID == PGID. Every descendant inherits that PGID (unless it calls
|
|
57
|
+
* `setsid` itself) and stays reachable even if it re-parents to PID 1.
|
|
58
|
+
* `-w` keeps the wrapper (and thus the exec) alive for the whole lifetime
|
|
59
|
+
* and propagates the child's exit status — without it `setsid` forks and
|
|
60
|
+
* returns immediately, so the exec would appear to finish while the real
|
|
61
|
+
* work keeps running.
|
|
62
|
+
* 2. The leader writes its own PID (`$$`) — the PGID — to a private file that
|
|
63
|
+
* only this process wrote, so the identity is kernel-owned and cannot be
|
|
64
|
+
* forged by another container process.
|
|
65
|
+
*
|
|
66
|
+
* If `setsid -w` is unavailable in the image (e.g. BusyBox), we degrade
|
|
67
|
+
* gracefully: the command runs directly and we record its PID so kill() can
|
|
68
|
+
* still signal it (descendant coverage is then best-effort). The probe
|
|
69
|
+
* `setsid -w true` also covers images without setsid at all.
|
|
70
|
+
*
|
|
71
|
+
* Positional args: $1 = pgid file path, $2 = user command. The script text is a
|
|
72
|
+
* static constant; runtime values travel only as argv, never interpolated into
|
|
73
|
+
* the command string.
|
|
74
|
+
*/
|
|
75
|
+
const SPAWN_WRAPPER = `
|
|
76
|
+
umask 077
|
|
77
|
+
d="\${1%/*}"
|
|
78
|
+
mkdir -p "$d" 2>/dev/null
|
|
79
|
+
chmod 700 "$d" 2>/dev/null
|
|
80
|
+
if setsid -w true >/dev/null 2>&1; then
|
|
81
|
+
exec setsid -w sh -c 'echo $$ > "$1" || exit 126; sh -c "$2"; ret=$?; rm -f "$1" 2>/dev/null; exit $ret' sh "$1" "$2"
|
|
82
|
+
fi
|
|
83
|
+
echo $$ > "$1" || exit 126
|
|
84
|
+
sh -c "$2"
|
|
85
|
+
ret=$?
|
|
86
|
+
rm -f "$1" 2>/dev/null
|
|
87
|
+
exit $ret
|
|
88
|
+
`;
|
|
89
|
+
/**
|
|
90
|
+
* Kill script: read the recorded PGID and SIGKILL the whole process group.
|
|
91
|
+
* A negative PID targets the kernel-owned process group, so descendants that
|
|
92
|
+
* re-parented to PID 1 are still caught. We SIGSTOP the
|
|
93
|
+
* group first to freeze fork races, then SIGKILL. The file may not exist yet if
|
|
94
|
+
* kill races the leader's first write, so we briefly wait for it.
|
|
95
|
+
*
|
|
96
|
+
* Positional arg: $1 = pgid file path (static script; no interpolation).
|
|
97
|
+
*/
|
|
98
|
+
const KILL_SCRIPT = `
|
|
99
|
+
f="$1"
|
|
100
|
+
i=0
|
|
101
|
+
while [ ! -r "$f" ] && [ "$i" -lt 40 ]; do sleep 0.05; i=$((i + 1)); done
|
|
102
|
+
# If the PGID was never recorded (file absent/unreadable after the wait, or
|
|
103
|
+
# empty), we have no group to signal or verify — report failure rather than
|
|
104
|
+
# falsely claiming the tree was terminated.
|
|
105
|
+
[ -r "$f" ] || exit 1
|
|
106
|
+
pgid=$(cat "$f" 2>/dev/null)
|
|
107
|
+
rm -f "$f" 2>/dev/null
|
|
108
|
+
[ -n "$pgid" ] || exit 1
|
|
109
|
+
kill -STOP -"$pgid" 2>/dev/null
|
|
110
|
+
kill -KILL -"$pgid" 2>/dev/null
|
|
111
|
+
# Fallback for images without setsid: the leader is not a group leader, so also
|
|
112
|
+
# signal it directly.
|
|
113
|
+
kill -KILL "$pgid" 2>/dev/null
|
|
114
|
+
# Verify the group is actually gone before reporting success. kill -0 probes
|
|
115
|
+
# for the group's existence without sending a signal, and we poll while it
|
|
116
|
+
# still reports the group alive. When the probe finally fails we must inspect
|
|
117
|
+
# why: ESRCH ("no such process") means every member was reaped, so report
|
|
118
|
+
# success; EPERM or any other error means termination is unconfirmed (e.g. a
|
|
119
|
+
# member dropped privileges and became unsignalable), so exit nonzero and let
|
|
120
|
+
# kill() report failure instead of falsely claiming the tree was terminated.
|
|
121
|
+
j=0
|
|
122
|
+
while err=$(kill -0 -"$pgid" 2>&1); do
|
|
123
|
+
j=$((j + 1))
|
|
124
|
+
[ "$j" -ge 40 ] && exit 1
|
|
125
|
+
sleep 0.05
|
|
126
|
+
done
|
|
127
|
+
case "$err" in
|
|
128
|
+
*[Ss]uch\\ process*) exit 0 ;;
|
|
129
|
+
*) exit 1 ;;
|
|
130
|
+
esac
|
|
131
|
+
`;
|
|
132
|
+
/**
|
|
32
133
|
* Wraps a Docker exec instance to conform to Mastra's ProcessHandle.
|
|
33
134
|
* Not exported — internal to this module.
|
|
34
135
|
*
|
|
@@ -48,13 +149,16 @@ var DockerProcessHandle = class extends _mastra_core_workspace.ProcessHandle {
|
|
|
48
149
|
_waitPromise = null;
|
|
49
150
|
_stdinStream = null;
|
|
50
151
|
_execStream = null;
|
|
51
|
-
|
|
152
|
+
/** @internal Container path of the file holding this process group's PGID. */
|
|
153
|
+
_pgidFile;
|
|
154
|
+
constructor(exec, container, startTime, stdinStream, pgidFile, options) {
|
|
52
155
|
super(options);
|
|
53
156
|
this.pid = exec.id;
|
|
54
157
|
this._exec = exec;
|
|
55
158
|
this._container = container;
|
|
56
159
|
this._startTime = startTime;
|
|
57
160
|
this._stdinStream = stdinStream;
|
|
161
|
+
this._pgidFile = pgidFile;
|
|
58
162
|
}
|
|
59
163
|
get exitCode() {
|
|
60
164
|
return this._exitCode;
|
|
@@ -85,37 +189,28 @@ var DockerProcessHandle = class extends _mastra_core_workspace.ProcessHandle {
|
|
|
85
189
|
async kill() {
|
|
86
190
|
if (this._exitCode !== void 0) return false;
|
|
87
191
|
try {
|
|
88
|
-
|
|
89
|
-
if (!info.Running || !info.Pid) {
|
|
90
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
91
|
-
info = await this._inspectExec();
|
|
92
|
-
}
|
|
93
|
-
if (!info.Running) {
|
|
94
|
-
this._killed = true;
|
|
95
|
-
this._destroyStream();
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
const pid = info.Pid;
|
|
99
|
-
if (!pid) {
|
|
100
|
-
this._killed = true;
|
|
101
|
-
this._destroyStream();
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
await (await this._container.exec({
|
|
192
|
+
const killExec = await this._container.exec({
|
|
105
193
|
Cmd: [
|
|
106
194
|
"sh",
|
|
107
195
|
"-c",
|
|
108
|
-
|
|
196
|
+
KILL_SCRIPT,
|
|
197
|
+
"sh",
|
|
198
|
+
this._pgidFile
|
|
109
199
|
],
|
|
110
200
|
AttachStdout: false,
|
|
111
201
|
AttachStderr: false
|
|
112
|
-
})
|
|
202
|
+
});
|
|
203
|
+
await killExec.start({});
|
|
204
|
+
let killInfo = await killExec.inspect();
|
|
205
|
+
while (killInfo.Running) {
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
207
|
+
killInfo = await killExec.inspect();
|
|
208
|
+
}
|
|
209
|
+
if (killInfo.ExitCode !== 0) throw new Error(`kill helper exited with code ${killInfo.ExitCode}`);
|
|
113
210
|
this._killed = true;
|
|
114
211
|
this._destroyStream();
|
|
115
212
|
return true;
|
|
116
213
|
} catch (error) {
|
|
117
|
-
this._killed = true;
|
|
118
|
-
this._destroyStream();
|
|
119
214
|
const msg = error instanceof Error ? error.message.toLowerCase() : "";
|
|
120
215
|
if (!msg.includes("no such process") && !msg.includes("esrch")) console.warn(`[DockerProcessManager] kill(${this.pid}) failed unexpectedly:`, error);
|
|
121
216
|
return false;
|
|
@@ -169,11 +264,15 @@ var DockerProcessManager = class extends _mastra_core_workspace.SandboxProcessMa
|
|
|
169
264
|
}
|
|
170
265
|
async spawn(command, options = {}) {
|
|
171
266
|
const container = this.container;
|
|
267
|
+
const pgidFile = `${PROC_DIR}/${(0, crypto.randomUUID)()}`;
|
|
172
268
|
const envArray = Object.entries({ ...options.env }).filter((entry) => entry[1] !== void 0).map(([k, v]) => `${k}=${v}`);
|
|
173
269
|
const exec = await container.exec({
|
|
174
270
|
Cmd: [
|
|
175
271
|
"sh",
|
|
176
272
|
"-c",
|
|
273
|
+
SPAWN_WRAPPER,
|
|
274
|
+
"sh",
|
|
275
|
+
pgidFile,
|
|
177
276
|
command
|
|
178
277
|
],
|
|
179
278
|
AttachStdout: true,
|
|
@@ -188,7 +287,7 @@ var DockerProcessManager = class extends _mastra_core_workspace.SandboxProcessMa
|
|
|
188
287
|
stdin: true
|
|
189
288
|
});
|
|
190
289
|
const startTime = Date.now();
|
|
191
|
-
const handle = new DockerProcessHandle(exec, container, startTime, stream, options);
|
|
290
|
+
const handle = new DockerProcessHandle(exec, container, startTime, stream, pgidFile, options);
|
|
192
291
|
handle._setExecStream(stream);
|
|
193
292
|
const waitPromise = new Promise((resolve) => {
|
|
194
293
|
const buffer = [];
|
|
@@ -264,8 +363,15 @@ var DockerProcessManager = class extends _mastra_core_workspace.SandboxProcessMa
|
|
|
264
363
|
if (handle.exitCode === void 0) {
|
|
265
364
|
handle._killed = true;
|
|
266
365
|
handle._timedOut = true;
|
|
267
|
-
|
|
268
|
-
|
|
366
|
+
const forceClose = () => {
|
|
367
|
+
if (handle.exitCode === void 0) {
|
|
368
|
+
handle._killed = true;
|
|
369
|
+
handle._destroyStream();
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
handle.kill().then((killed) => {
|
|
373
|
+
if (!killed) forceClose();
|
|
374
|
+
}).catch(forceClose);
|
|
269
375
|
}
|
|
270
376
|
}, resolvedTimeout);
|
|
271
377
|
waitPromise.then(() => clearTimeout(timer));
|
|
@@ -358,12 +464,14 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
|
|
|
358
464
|
_cpuQuota;
|
|
359
465
|
_cpuPeriod;
|
|
360
466
|
_pidsLimit;
|
|
467
|
+
_init;
|
|
361
468
|
_readonlyRootfs;
|
|
362
469
|
_capDrop;
|
|
363
470
|
_capAdd;
|
|
364
471
|
_securityOpt;
|
|
365
472
|
_ulimits;
|
|
366
473
|
_tmpfs;
|
|
474
|
+
_mounts;
|
|
367
475
|
_labels;
|
|
368
476
|
_instructionsOverride;
|
|
369
477
|
_constructorOptions;
|
|
@@ -397,12 +505,14 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
|
|
|
397
505
|
this._cpuQuota = options.cpuQuota;
|
|
398
506
|
this._cpuPeriod = options.cpuPeriod;
|
|
399
507
|
this._pidsLimit = options.pidsLimit;
|
|
508
|
+
this._init = options.init ?? true;
|
|
400
509
|
this._readonlyRootfs = options.readonlyRootfs;
|
|
401
510
|
this._capDrop = options.capDrop;
|
|
402
511
|
this._capAdd = options.capAdd;
|
|
403
512
|
this._securityOpt = options.securityOpt;
|
|
404
513
|
this._ulimits = options.ulimits;
|
|
405
514
|
this._tmpfs = options.tmpfs;
|
|
515
|
+
this._mounts = options.mounts;
|
|
406
516
|
this.setWorkingDirectory(options.workingDirectory ?? options.workingDir ?? "/workspace");
|
|
407
517
|
this._labels = {
|
|
408
518
|
...options.labels,
|
|
@@ -484,12 +594,14 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
|
|
|
484
594
|
CpuQuota: this._cpuQuota,
|
|
485
595
|
CpuPeriod: this._cpuPeriod,
|
|
486
596
|
PidsLimit: this._pidsLimit,
|
|
597
|
+
Init: this._init,
|
|
487
598
|
ReadonlyRootfs: this._readonlyRootfs,
|
|
488
599
|
CapDrop: this._capDrop,
|
|
489
600
|
CapAdd: this._capAdd,
|
|
490
601
|
SecurityOpt: this._securityOpt,
|
|
491
602
|
Ulimits: this._ulimits?.map(toDockerUlimit),
|
|
492
|
-
Tmpfs: this._tmpfs
|
|
603
|
+
Tmpfs: this._tmpfs,
|
|
604
|
+
Mounts: this._mounts?.map(toDockerMount)
|
|
493
605
|
},
|
|
494
606
|
OpenStdin: true,
|
|
495
607
|
Tty: false
|
|
@@ -543,7 +655,8 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
|
|
|
543
655
|
["CapAdd", this._capAdd],
|
|
544
656
|
["SecurityOpt", this._securityOpt],
|
|
545
657
|
["Ulimits", this._ulimits],
|
|
546
|
-
["Tmpfs", this._tmpfs]
|
|
658
|
+
["Tmpfs", this._tmpfs],
|
|
659
|
+
["Mounts", this._mounts?.map(toDockerMount)]
|
|
547
660
|
];
|
|
548
661
|
if (this._privilegedWasSet || hostConfig?.Privileged === true) entries.unshift(["Privileged", this._privileged]);
|
|
549
662
|
return entries.filter((entry) => isPresentHostConfigValue(entry[1]));
|
|
@@ -589,32 +702,60 @@ var DockerSandbox = class DockerSandbox extends _mastra_core_workspace.MastraSan
|
|
|
589
702
|
* - Existing destinations are overwritten (contents and mode).
|
|
590
703
|
* - Exact bytes are preserved for both `string` and `Buffer` content,
|
|
591
704
|
* including empty files and binary data.
|
|
592
|
-
* - New files
|
|
593
|
-
*
|
|
705
|
+
* - New files use the per-file `mode` when provided (validated integer
|
|
706
|
+
* `0o001`–`0o777`), otherwise `0644`; directories created implicitly default
|
|
707
|
+
* to Docker's `0755`. Overwriting a file replaces its mode with the
|
|
708
|
+
* requested `mode` (or `0644` when omitted).
|
|
594
709
|
* - Not atomic across files: on failure the promise rejects and earlier or
|
|
595
710
|
* partially written files may remain.
|
|
596
711
|
*
|
|
712
|
+
* Cancellation (`options.abortSignal`):
|
|
713
|
+
* - If the signal is already aborted, rejects with {@link SandboxAbortError}
|
|
714
|
+
* before creating the archive or starting the upload.
|
|
715
|
+
* - If the signal aborts during transfer, the underlying `putArchive` request
|
|
716
|
+
* is terminated by destroying the tar stream (which ends the request body),
|
|
717
|
+
* and the promise rejects with {@link SandboxAbortError}.
|
|
718
|
+
* - Upload and cancellation race: an upload that completes before the abort is
|
|
719
|
+
* observed resolves normally.
|
|
720
|
+
* - No rollback: files Docker already received or extracted may remain. The
|
|
721
|
+
* daemon may continue extraction after rejection, so the caller is
|
|
722
|
+
* responsible for any cleanup or sandbox disposal.
|
|
723
|
+
* - Behavior is unchanged when no signal is supplied.
|
|
724
|
+
*
|
|
597
725
|
* @throws {SandboxNotReadyError} If the sandbox has not been started.
|
|
726
|
+
* @throws {SandboxAbortError} If the write is cancelled via `options.abortSignal`.
|
|
598
727
|
* @throws {SandboxError} If the archive upload fails.
|
|
599
728
|
*/
|
|
600
|
-
async writeFiles(files) {
|
|
729
|
+
async writeFiles(files, options) {
|
|
601
730
|
const container = this.container;
|
|
731
|
+
const signal = options?.abortSignal;
|
|
732
|
+
if (signal?.aborted) throw new _mastra_core_workspace.SandboxAbortError("writeFiles");
|
|
602
733
|
if (files.length === 0) return;
|
|
603
734
|
const pack = (0, tar_stream.pack)();
|
|
604
735
|
for (const file of files) {
|
|
736
|
+
if (file.mode !== void 0) (0, _mastra_core_workspace.validateSandboxFileMode)(file.mode);
|
|
605
737
|
const resolved = path.posix.isAbsolute(file.path) ? path.posix.normalize(file.path) : path.posix.resolve(this.workingDirectory, file.path);
|
|
606
738
|
const data = Buffer.isBuffer(file.content) ? file.content : Buffer.from(file.content);
|
|
739
|
+
const mode = file.mode ?? 420;
|
|
607
740
|
pack.entry({
|
|
608
741
|
name: resolved.replace(/^\/+/, ""),
|
|
609
742
|
size: data.length,
|
|
610
|
-
mode
|
|
743
|
+
mode
|
|
611
744
|
}, data);
|
|
612
745
|
}
|
|
613
746
|
pack.finalize();
|
|
747
|
+
const onAbort = () => pack.destroy();
|
|
748
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
614
749
|
try {
|
|
615
|
-
await container.putArchive(pack, {
|
|
750
|
+
await container.putArchive(pack, {
|
|
751
|
+
path: "/",
|
|
752
|
+
abortSignal: signal
|
|
753
|
+
});
|
|
616
754
|
} catch (error) {
|
|
755
|
+
if (signal?.aborted) throw new _mastra_core_workspace.SandboxAbortError("writeFiles");
|
|
617
756
|
throw new _mastra_core_workspace.SandboxError(`Failed to write files to sandbox: ${error instanceof Error ? error.message : String(error)}`, "EXECUTION_FAILED", { reason: "write_files_failed" });
|
|
757
|
+
} finally {
|
|
758
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
618
759
|
}
|
|
619
760
|
}
|
|
620
761
|
getInstructions(opts) {
|
|
@@ -791,6 +932,31 @@ function toDockerUlimit(ulimit) {
|
|
|
791
932
|
Hard: ulimit.hard
|
|
792
933
|
};
|
|
793
934
|
}
|
|
935
|
+
function toDockerMount(mount) {
|
|
936
|
+
const settings = {
|
|
937
|
+
Type: mount.type,
|
|
938
|
+
Target: mount.target,
|
|
939
|
+
Source: mount.type === "tmpfs" ? "" : mount.source
|
|
940
|
+
};
|
|
941
|
+
if (mount.readOnly !== void 0) settings.ReadOnly = mount.readOnly;
|
|
942
|
+
if (mount.type === "volume" && mount.volumeOptions) {
|
|
943
|
+
const { subpath, noCopy, labels } = mount.volumeOptions;
|
|
944
|
+
const volumeOptions = {};
|
|
945
|
+
if (subpath !== void 0) volumeOptions.Subpath = subpath;
|
|
946
|
+
if (noCopy !== void 0) volumeOptions.NoCopy = noCopy;
|
|
947
|
+
if (labels !== void 0) volumeOptions.Labels = labels;
|
|
948
|
+
if (Object.keys(volumeOptions).length > 0) settings.VolumeOptions = volumeOptions;
|
|
949
|
+
}
|
|
950
|
+
if (mount.type === "bind" && mount.bindOptions?.propagation !== void 0) settings.BindOptions = { Propagation: mount.bindOptions.propagation };
|
|
951
|
+
if (mount.type === "tmpfs" && mount.tmpfsOptions) {
|
|
952
|
+
const { sizeBytes, mode } = mount.tmpfsOptions;
|
|
953
|
+
const tmpfsOptions = {};
|
|
954
|
+
if (sizeBytes !== void 0) tmpfsOptions.SizeBytes = sizeBytes;
|
|
955
|
+
if (mode !== void 0) tmpfsOptions.Mode = mode;
|
|
956
|
+
if (Object.keys(tmpfsOptions).length > 0) settings.TmpfsOptions = tmpfsOptions;
|
|
957
|
+
}
|
|
958
|
+
return settings;
|
|
959
|
+
}
|
|
794
960
|
function toDockerSandboxOptionName(field) {
|
|
795
961
|
return {
|
|
796
962
|
Privileged: "privileged",
|
|
@@ -805,7 +971,8 @@ function toDockerSandboxOptionName(field) {
|
|
|
805
971
|
CapAdd: "capAdd",
|
|
806
972
|
SecurityOpt: "securityOpt",
|
|
807
973
|
Ulimits: "ulimits",
|
|
808
|
-
Tmpfs: "tmpfs"
|
|
974
|
+
Tmpfs: "tmpfs",
|
|
975
|
+
Mounts: "mounts"
|
|
809
976
|
}[field] ?? String(field);
|
|
810
977
|
}
|
|
811
978
|
//#endregion
|
|
@@ -916,6 +1083,87 @@ const dockerSandboxProvider = {
|
|
|
916
1083
|
type: "object",
|
|
917
1084
|
description: "tmpfs mount paths with options",
|
|
918
1085
|
additionalProperties: { type: "string" }
|
|
1086
|
+
},
|
|
1087
|
+
mounts: {
|
|
1088
|
+
type: "array",
|
|
1089
|
+
description: "Mounts mapped 1:1 onto Docker HostConfig.Mounts (supports volume subpath)",
|
|
1090
|
+
items: { oneOf: [
|
|
1091
|
+
{
|
|
1092
|
+
type: "object",
|
|
1093
|
+
required: [
|
|
1094
|
+
"type",
|
|
1095
|
+
"target",
|
|
1096
|
+
"source"
|
|
1097
|
+
],
|
|
1098
|
+
additionalProperties: false,
|
|
1099
|
+
properties: {
|
|
1100
|
+
type: { const: "volume" },
|
|
1101
|
+
target: { type: "string" },
|
|
1102
|
+
source: { type: "string" },
|
|
1103
|
+
readOnly: { type: "boolean" },
|
|
1104
|
+
volumeOptions: {
|
|
1105
|
+
type: "object",
|
|
1106
|
+
additionalProperties: false,
|
|
1107
|
+
properties: {
|
|
1108
|
+
subpath: { type: "string" },
|
|
1109
|
+
noCopy: { type: "boolean" },
|
|
1110
|
+
labels: {
|
|
1111
|
+
type: "object",
|
|
1112
|
+
additionalProperties: { type: "string" }
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
type: "object",
|
|
1120
|
+
required: [
|
|
1121
|
+
"type",
|
|
1122
|
+
"target",
|
|
1123
|
+
"source"
|
|
1124
|
+
],
|
|
1125
|
+
additionalProperties: false,
|
|
1126
|
+
properties: {
|
|
1127
|
+
type: { const: "bind" },
|
|
1128
|
+
target: { type: "string" },
|
|
1129
|
+
source: { type: "string" },
|
|
1130
|
+
readOnly: { type: "boolean" },
|
|
1131
|
+
bindOptions: {
|
|
1132
|
+
type: "object",
|
|
1133
|
+
additionalProperties: false,
|
|
1134
|
+
properties: { propagation: {
|
|
1135
|
+
type: "string",
|
|
1136
|
+
enum: [
|
|
1137
|
+
"private",
|
|
1138
|
+
"rprivate",
|
|
1139
|
+
"shared",
|
|
1140
|
+
"rshared",
|
|
1141
|
+
"slave",
|
|
1142
|
+
"rslave"
|
|
1143
|
+
]
|
|
1144
|
+
} }
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
type: "object",
|
|
1150
|
+
required: ["type", "target"],
|
|
1151
|
+
additionalProperties: false,
|
|
1152
|
+
properties: {
|
|
1153
|
+
type: { const: "tmpfs" },
|
|
1154
|
+
target: { type: "string" },
|
|
1155
|
+
readOnly: { type: "boolean" },
|
|
1156
|
+
tmpfsOptions: {
|
|
1157
|
+
type: "object",
|
|
1158
|
+
additionalProperties: false,
|
|
1159
|
+
properties: {
|
|
1160
|
+
sizeBytes: { type: "number" },
|
|
1161
|
+
mode: { type: "number" }
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
] }
|
|
919
1167
|
}
|
|
920
1168
|
}
|
|
921
1169
|
},
|