@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/dist/index.js
CHANGED
|
@@ -1,10 +1,111 @@
|
|
|
1
1
|
import { posix } from "path";
|
|
2
2
|
import { isDeepStrictEqual } from "util";
|
|
3
|
-
import { MastraSandbox, ProcessHandle, SandboxError, SandboxNotReadyError, SandboxProcessManager } from "@mastra/core/workspace";
|
|
3
|
+
import { MastraSandbox, ProcessHandle, SandboxAbortError, SandboxError, SandboxNotReadyError, SandboxProcessManager, validateSandboxFileMode } from "@mastra/core/workspace";
|
|
4
4
|
import Docker from "dockerode";
|
|
5
5
|
import { pack } from "tar-stream";
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
6
7
|
//#region src/sandbox/process-manager.ts
|
|
7
8
|
/**
|
|
9
|
+
* Docker Process Manager
|
|
10
|
+
*
|
|
11
|
+
* Implements SandboxProcessManager for Docker containers.
|
|
12
|
+
* Uses `container.exec()` to run commands inside a long-lived container.
|
|
13
|
+
* Each spawned process gets a dedicated exec instance with separate
|
|
14
|
+
* stdout/stderr streams.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Directory (inside the container) where each spawned process records the PGID
|
|
18
|
+
* of its process group. Created with mode 700 so only the (root) exec user can
|
|
19
|
+
* write the PGID files.
|
|
20
|
+
*/
|
|
21
|
+
const PROC_DIR = "/tmp/.mastra-proc";
|
|
22
|
+
/**
|
|
23
|
+
* Wrapper (run as the exec command) that places the user command in its own
|
|
24
|
+
* process group and records the group's PGID so kill() can signal the whole
|
|
25
|
+
* group later.
|
|
26
|
+
*
|
|
27
|
+
* Docker's exec-inspect `Pid` is a host/daemon-namespace PID and cannot be used
|
|
28
|
+
* with an in-container `kill`, so we need a container-namespace identity. We use
|
|
29
|
+
* a *kernel-enforced* process group as that identity:
|
|
30
|
+
*
|
|
31
|
+
* 1. `setsid -w` re-execs the command as a new session/process-group leader,
|
|
32
|
+
* so its PID == PGID. Every descendant inherits that PGID (unless it calls
|
|
33
|
+
* `setsid` itself) and stays reachable even if it re-parents to PID 1.
|
|
34
|
+
* `-w` keeps the wrapper (and thus the exec) alive for the whole lifetime
|
|
35
|
+
* and propagates the child's exit status — without it `setsid` forks and
|
|
36
|
+
* returns immediately, so the exec would appear to finish while the real
|
|
37
|
+
* work keeps running.
|
|
38
|
+
* 2. The leader writes its own PID (`$$`) — the PGID — to a private file that
|
|
39
|
+
* only this process wrote, so the identity is kernel-owned and cannot be
|
|
40
|
+
* forged by another container process.
|
|
41
|
+
*
|
|
42
|
+
* If `setsid -w` is unavailable in the image (e.g. BusyBox), we degrade
|
|
43
|
+
* gracefully: the command runs directly and we record its PID so kill() can
|
|
44
|
+
* still signal it (descendant coverage is then best-effort). The probe
|
|
45
|
+
* `setsid -w true` also covers images without setsid at all.
|
|
46
|
+
*
|
|
47
|
+
* Positional args: $1 = pgid file path, $2 = user command. The script text is a
|
|
48
|
+
* static constant; runtime values travel only as argv, never interpolated into
|
|
49
|
+
* the command string.
|
|
50
|
+
*/
|
|
51
|
+
const SPAWN_WRAPPER = `
|
|
52
|
+
umask 077
|
|
53
|
+
d="\${1%/*}"
|
|
54
|
+
mkdir -p "$d" 2>/dev/null
|
|
55
|
+
chmod 700 "$d" 2>/dev/null
|
|
56
|
+
if setsid -w true >/dev/null 2>&1; then
|
|
57
|
+
exec setsid -w sh -c 'echo $$ > "$1" || exit 126; sh -c "$2"; ret=$?; rm -f "$1" 2>/dev/null; exit $ret' sh "$1" "$2"
|
|
58
|
+
fi
|
|
59
|
+
echo $$ > "$1" || exit 126
|
|
60
|
+
sh -c "$2"
|
|
61
|
+
ret=$?
|
|
62
|
+
rm -f "$1" 2>/dev/null
|
|
63
|
+
exit $ret
|
|
64
|
+
`;
|
|
65
|
+
/**
|
|
66
|
+
* Kill script: read the recorded PGID and SIGKILL the whole process group.
|
|
67
|
+
* A negative PID targets the kernel-owned process group, so descendants that
|
|
68
|
+
* re-parented to PID 1 are still caught. We SIGSTOP the
|
|
69
|
+
* group first to freeze fork races, then SIGKILL. The file may not exist yet if
|
|
70
|
+
* kill races the leader's first write, so we briefly wait for it.
|
|
71
|
+
*
|
|
72
|
+
* Positional arg: $1 = pgid file path (static script; no interpolation).
|
|
73
|
+
*/
|
|
74
|
+
const KILL_SCRIPT = `
|
|
75
|
+
f="$1"
|
|
76
|
+
i=0
|
|
77
|
+
while [ ! -r "$f" ] && [ "$i" -lt 40 ]; do sleep 0.05; i=$((i + 1)); done
|
|
78
|
+
# If the PGID was never recorded (file absent/unreadable after the wait, or
|
|
79
|
+
# empty), we have no group to signal or verify — report failure rather than
|
|
80
|
+
# falsely claiming the tree was terminated.
|
|
81
|
+
[ -r "$f" ] || exit 1
|
|
82
|
+
pgid=$(cat "$f" 2>/dev/null)
|
|
83
|
+
rm -f "$f" 2>/dev/null
|
|
84
|
+
[ -n "$pgid" ] || exit 1
|
|
85
|
+
kill -STOP -"$pgid" 2>/dev/null
|
|
86
|
+
kill -KILL -"$pgid" 2>/dev/null
|
|
87
|
+
# Fallback for images without setsid: the leader is not a group leader, so also
|
|
88
|
+
# signal it directly.
|
|
89
|
+
kill -KILL "$pgid" 2>/dev/null
|
|
90
|
+
# Verify the group is actually gone before reporting success. kill -0 probes
|
|
91
|
+
# for the group's existence without sending a signal, and we poll while it
|
|
92
|
+
# still reports the group alive. When the probe finally fails we must inspect
|
|
93
|
+
# why: ESRCH ("no such process") means every member was reaped, so report
|
|
94
|
+
# success; EPERM or any other error means termination is unconfirmed (e.g. a
|
|
95
|
+
# member dropped privileges and became unsignalable), so exit nonzero and let
|
|
96
|
+
# kill() report failure instead of falsely claiming the tree was terminated.
|
|
97
|
+
j=0
|
|
98
|
+
while err=$(kill -0 -"$pgid" 2>&1); do
|
|
99
|
+
j=$((j + 1))
|
|
100
|
+
[ "$j" -ge 40 ] && exit 1
|
|
101
|
+
sleep 0.05
|
|
102
|
+
done
|
|
103
|
+
case "$err" in
|
|
104
|
+
*[Ss]uch\\ process*) exit 0 ;;
|
|
105
|
+
*) exit 1 ;;
|
|
106
|
+
esac
|
|
107
|
+
`;
|
|
108
|
+
/**
|
|
8
109
|
* Wraps a Docker exec instance to conform to Mastra's ProcessHandle.
|
|
9
110
|
* Not exported — internal to this module.
|
|
10
111
|
*
|
|
@@ -24,13 +125,16 @@ var DockerProcessHandle = class extends ProcessHandle {
|
|
|
24
125
|
_waitPromise = null;
|
|
25
126
|
_stdinStream = null;
|
|
26
127
|
_execStream = null;
|
|
27
|
-
|
|
128
|
+
/** @internal Container path of the file holding this process group's PGID. */
|
|
129
|
+
_pgidFile;
|
|
130
|
+
constructor(exec, container, startTime, stdinStream, pgidFile, options) {
|
|
28
131
|
super(options);
|
|
29
132
|
this.pid = exec.id;
|
|
30
133
|
this._exec = exec;
|
|
31
134
|
this._container = container;
|
|
32
135
|
this._startTime = startTime;
|
|
33
136
|
this._stdinStream = stdinStream;
|
|
137
|
+
this._pgidFile = pgidFile;
|
|
34
138
|
}
|
|
35
139
|
get exitCode() {
|
|
36
140
|
return this._exitCode;
|
|
@@ -61,37 +165,28 @@ var DockerProcessHandle = class extends ProcessHandle {
|
|
|
61
165
|
async kill() {
|
|
62
166
|
if (this._exitCode !== void 0) return false;
|
|
63
167
|
try {
|
|
64
|
-
|
|
65
|
-
if (!info.Running || !info.Pid) {
|
|
66
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
67
|
-
info = await this._inspectExec();
|
|
68
|
-
}
|
|
69
|
-
if (!info.Running) {
|
|
70
|
-
this._killed = true;
|
|
71
|
-
this._destroyStream();
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
const pid = info.Pid;
|
|
75
|
-
if (!pid) {
|
|
76
|
-
this._killed = true;
|
|
77
|
-
this._destroyStream();
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
await (await this._container.exec({
|
|
168
|
+
const killExec = await this._container.exec({
|
|
81
169
|
Cmd: [
|
|
82
170
|
"sh",
|
|
83
171
|
"-c",
|
|
84
|
-
|
|
172
|
+
KILL_SCRIPT,
|
|
173
|
+
"sh",
|
|
174
|
+
this._pgidFile
|
|
85
175
|
],
|
|
86
176
|
AttachStdout: false,
|
|
87
177
|
AttachStderr: false
|
|
88
|
-
})
|
|
178
|
+
});
|
|
179
|
+
await killExec.start({});
|
|
180
|
+
let killInfo = await killExec.inspect();
|
|
181
|
+
while (killInfo.Running) {
|
|
182
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
183
|
+
killInfo = await killExec.inspect();
|
|
184
|
+
}
|
|
185
|
+
if (killInfo.ExitCode !== 0) throw new Error(`kill helper exited with code ${killInfo.ExitCode}`);
|
|
89
186
|
this._killed = true;
|
|
90
187
|
this._destroyStream();
|
|
91
188
|
return true;
|
|
92
189
|
} catch (error) {
|
|
93
|
-
this._killed = true;
|
|
94
|
-
this._destroyStream();
|
|
95
190
|
const msg = error instanceof Error ? error.message.toLowerCase() : "";
|
|
96
191
|
if (!msg.includes("no such process") && !msg.includes("esrch")) console.warn(`[DockerProcessManager] kill(${this.pid}) failed unexpectedly:`, error);
|
|
97
192
|
return false;
|
|
@@ -145,11 +240,15 @@ var DockerProcessManager = class extends SandboxProcessManager {
|
|
|
145
240
|
}
|
|
146
241
|
async spawn(command, options = {}) {
|
|
147
242
|
const container = this.container;
|
|
243
|
+
const pgidFile = `${PROC_DIR}/${randomUUID()}`;
|
|
148
244
|
const envArray = Object.entries({ ...options.env }).filter((entry) => entry[1] !== void 0).map(([k, v]) => `${k}=${v}`);
|
|
149
245
|
const exec = await container.exec({
|
|
150
246
|
Cmd: [
|
|
151
247
|
"sh",
|
|
152
248
|
"-c",
|
|
249
|
+
SPAWN_WRAPPER,
|
|
250
|
+
"sh",
|
|
251
|
+
pgidFile,
|
|
153
252
|
command
|
|
154
253
|
],
|
|
155
254
|
AttachStdout: true,
|
|
@@ -164,7 +263,7 @@ var DockerProcessManager = class extends SandboxProcessManager {
|
|
|
164
263
|
stdin: true
|
|
165
264
|
});
|
|
166
265
|
const startTime = Date.now();
|
|
167
|
-
const handle = new DockerProcessHandle(exec, container, startTime, stream, options);
|
|
266
|
+
const handle = new DockerProcessHandle(exec, container, startTime, stream, pgidFile, options);
|
|
168
267
|
handle._setExecStream(stream);
|
|
169
268
|
const waitPromise = new Promise((resolve) => {
|
|
170
269
|
const buffer = [];
|
|
@@ -240,8 +339,15 @@ var DockerProcessManager = class extends SandboxProcessManager {
|
|
|
240
339
|
if (handle.exitCode === void 0) {
|
|
241
340
|
handle._killed = true;
|
|
242
341
|
handle._timedOut = true;
|
|
243
|
-
|
|
244
|
-
|
|
342
|
+
const forceClose = () => {
|
|
343
|
+
if (handle.exitCode === void 0) {
|
|
344
|
+
handle._killed = true;
|
|
345
|
+
handle._destroyStream();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
handle.kill().then((killed) => {
|
|
349
|
+
if (!killed) forceClose();
|
|
350
|
+
}).catch(forceClose);
|
|
245
351
|
}
|
|
246
352
|
}, resolvedTimeout);
|
|
247
353
|
waitPromise.then(() => clearTimeout(timer));
|
|
@@ -334,12 +440,14 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
|
|
|
334
440
|
_cpuQuota;
|
|
335
441
|
_cpuPeriod;
|
|
336
442
|
_pidsLimit;
|
|
443
|
+
_init;
|
|
337
444
|
_readonlyRootfs;
|
|
338
445
|
_capDrop;
|
|
339
446
|
_capAdd;
|
|
340
447
|
_securityOpt;
|
|
341
448
|
_ulimits;
|
|
342
449
|
_tmpfs;
|
|
450
|
+
_mounts;
|
|
343
451
|
_labels;
|
|
344
452
|
_instructionsOverride;
|
|
345
453
|
_constructorOptions;
|
|
@@ -373,12 +481,14 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
|
|
|
373
481
|
this._cpuQuota = options.cpuQuota;
|
|
374
482
|
this._cpuPeriod = options.cpuPeriod;
|
|
375
483
|
this._pidsLimit = options.pidsLimit;
|
|
484
|
+
this._init = options.init ?? true;
|
|
376
485
|
this._readonlyRootfs = options.readonlyRootfs;
|
|
377
486
|
this._capDrop = options.capDrop;
|
|
378
487
|
this._capAdd = options.capAdd;
|
|
379
488
|
this._securityOpt = options.securityOpt;
|
|
380
489
|
this._ulimits = options.ulimits;
|
|
381
490
|
this._tmpfs = options.tmpfs;
|
|
491
|
+
this._mounts = options.mounts;
|
|
382
492
|
this.setWorkingDirectory(options.workingDirectory ?? options.workingDir ?? "/workspace");
|
|
383
493
|
this._labels = {
|
|
384
494
|
...options.labels,
|
|
@@ -460,12 +570,14 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
|
|
|
460
570
|
CpuQuota: this._cpuQuota,
|
|
461
571
|
CpuPeriod: this._cpuPeriod,
|
|
462
572
|
PidsLimit: this._pidsLimit,
|
|
573
|
+
Init: this._init,
|
|
463
574
|
ReadonlyRootfs: this._readonlyRootfs,
|
|
464
575
|
CapDrop: this._capDrop,
|
|
465
576
|
CapAdd: this._capAdd,
|
|
466
577
|
SecurityOpt: this._securityOpt,
|
|
467
578
|
Ulimits: this._ulimits?.map(toDockerUlimit),
|
|
468
|
-
Tmpfs: this._tmpfs
|
|
579
|
+
Tmpfs: this._tmpfs,
|
|
580
|
+
Mounts: this._mounts?.map(toDockerMount)
|
|
469
581
|
},
|
|
470
582
|
OpenStdin: true,
|
|
471
583
|
Tty: false
|
|
@@ -519,7 +631,8 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
|
|
|
519
631
|
["CapAdd", this._capAdd],
|
|
520
632
|
["SecurityOpt", this._securityOpt],
|
|
521
633
|
["Ulimits", this._ulimits],
|
|
522
|
-
["Tmpfs", this._tmpfs]
|
|
634
|
+
["Tmpfs", this._tmpfs],
|
|
635
|
+
["Mounts", this._mounts?.map(toDockerMount)]
|
|
523
636
|
];
|
|
524
637
|
if (this._privilegedWasSet || hostConfig?.Privileged === true) entries.unshift(["Privileged", this._privileged]);
|
|
525
638
|
return entries.filter((entry) => isPresentHostConfigValue(entry[1]));
|
|
@@ -565,32 +678,60 @@ var DockerSandbox = class DockerSandbox extends MastraSandbox {
|
|
|
565
678
|
* - Existing destinations are overwritten (contents and mode).
|
|
566
679
|
* - Exact bytes are preserved for both `string` and `Buffer` content,
|
|
567
680
|
* including empty files and binary data.
|
|
568
|
-
* - New files
|
|
569
|
-
*
|
|
681
|
+
* - New files use the per-file `mode` when provided (validated integer
|
|
682
|
+
* `0o001`–`0o777`), otherwise `0644`; directories created implicitly default
|
|
683
|
+
* to Docker's `0755`. Overwriting a file replaces its mode with the
|
|
684
|
+
* requested `mode` (or `0644` when omitted).
|
|
570
685
|
* - Not atomic across files: on failure the promise rejects and earlier or
|
|
571
686
|
* partially written files may remain.
|
|
572
687
|
*
|
|
688
|
+
* Cancellation (`options.abortSignal`):
|
|
689
|
+
* - If the signal is already aborted, rejects with {@link SandboxAbortError}
|
|
690
|
+
* before creating the archive or starting the upload.
|
|
691
|
+
* - If the signal aborts during transfer, the underlying `putArchive` request
|
|
692
|
+
* is terminated by destroying the tar stream (which ends the request body),
|
|
693
|
+
* and the promise rejects with {@link SandboxAbortError}.
|
|
694
|
+
* - Upload and cancellation race: an upload that completes before the abort is
|
|
695
|
+
* observed resolves normally.
|
|
696
|
+
* - No rollback: files Docker already received or extracted may remain. The
|
|
697
|
+
* daemon may continue extraction after rejection, so the caller is
|
|
698
|
+
* responsible for any cleanup or sandbox disposal.
|
|
699
|
+
* - Behavior is unchanged when no signal is supplied.
|
|
700
|
+
*
|
|
573
701
|
* @throws {SandboxNotReadyError} If the sandbox has not been started.
|
|
702
|
+
* @throws {SandboxAbortError} If the write is cancelled via `options.abortSignal`.
|
|
574
703
|
* @throws {SandboxError} If the archive upload fails.
|
|
575
704
|
*/
|
|
576
|
-
async writeFiles(files) {
|
|
705
|
+
async writeFiles(files, options) {
|
|
577
706
|
const container = this.container;
|
|
707
|
+
const signal = options?.abortSignal;
|
|
708
|
+
if (signal?.aborted) throw new SandboxAbortError("writeFiles");
|
|
578
709
|
if (files.length === 0) return;
|
|
579
710
|
const pack$1 = pack();
|
|
580
711
|
for (const file of files) {
|
|
712
|
+
if (file.mode !== void 0) validateSandboxFileMode(file.mode);
|
|
581
713
|
const resolved = posix.isAbsolute(file.path) ? posix.normalize(file.path) : posix.resolve(this.workingDirectory, file.path);
|
|
582
714
|
const data = Buffer.isBuffer(file.content) ? file.content : Buffer.from(file.content);
|
|
715
|
+
const mode = file.mode ?? 420;
|
|
583
716
|
pack$1.entry({
|
|
584
717
|
name: resolved.replace(/^\/+/, ""),
|
|
585
718
|
size: data.length,
|
|
586
|
-
mode
|
|
719
|
+
mode
|
|
587
720
|
}, data);
|
|
588
721
|
}
|
|
589
722
|
pack$1.finalize();
|
|
723
|
+
const onAbort = () => pack$1.destroy();
|
|
724
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
590
725
|
try {
|
|
591
|
-
await container.putArchive(pack$1, {
|
|
726
|
+
await container.putArchive(pack$1, {
|
|
727
|
+
path: "/",
|
|
728
|
+
abortSignal: signal
|
|
729
|
+
});
|
|
592
730
|
} catch (error) {
|
|
731
|
+
if (signal?.aborted) throw new SandboxAbortError("writeFiles");
|
|
593
732
|
throw new SandboxError(`Failed to write files to sandbox: ${error instanceof Error ? error.message : String(error)}`, "EXECUTION_FAILED", { reason: "write_files_failed" });
|
|
733
|
+
} finally {
|
|
734
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
594
735
|
}
|
|
595
736
|
}
|
|
596
737
|
getInstructions(opts) {
|
|
@@ -767,6 +908,31 @@ function toDockerUlimit(ulimit) {
|
|
|
767
908
|
Hard: ulimit.hard
|
|
768
909
|
};
|
|
769
910
|
}
|
|
911
|
+
function toDockerMount(mount) {
|
|
912
|
+
const settings = {
|
|
913
|
+
Type: mount.type,
|
|
914
|
+
Target: mount.target,
|
|
915
|
+
Source: mount.type === "tmpfs" ? "" : mount.source
|
|
916
|
+
};
|
|
917
|
+
if (mount.readOnly !== void 0) settings.ReadOnly = mount.readOnly;
|
|
918
|
+
if (mount.type === "volume" && mount.volumeOptions) {
|
|
919
|
+
const { subpath, noCopy, labels } = mount.volumeOptions;
|
|
920
|
+
const volumeOptions = {};
|
|
921
|
+
if (subpath !== void 0) volumeOptions.Subpath = subpath;
|
|
922
|
+
if (noCopy !== void 0) volumeOptions.NoCopy = noCopy;
|
|
923
|
+
if (labels !== void 0) volumeOptions.Labels = labels;
|
|
924
|
+
if (Object.keys(volumeOptions).length > 0) settings.VolumeOptions = volumeOptions;
|
|
925
|
+
}
|
|
926
|
+
if (mount.type === "bind" && mount.bindOptions?.propagation !== void 0) settings.BindOptions = { Propagation: mount.bindOptions.propagation };
|
|
927
|
+
if (mount.type === "tmpfs" && mount.tmpfsOptions) {
|
|
928
|
+
const { sizeBytes, mode } = mount.tmpfsOptions;
|
|
929
|
+
const tmpfsOptions = {};
|
|
930
|
+
if (sizeBytes !== void 0) tmpfsOptions.SizeBytes = sizeBytes;
|
|
931
|
+
if (mode !== void 0) tmpfsOptions.Mode = mode;
|
|
932
|
+
if (Object.keys(tmpfsOptions).length > 0) settings.TmpfsOptions = tmpfsOptions;
|
|
933
|
+
}
|
|
934
|
+
return settings;
|
|
935
|
+
}
|
|
770
936
|
function toDockerSandboxOptionName(field) {
|
|
771
937
|
return {
|
|
772
938
|
Privileged: "privileged",
|
|
@@ -781,7 +947,8 @@ function toDockerSandboxOptionName(field) {
|
|
|
781
947
|
CapAdd: "capAdd",
|
|
782
948
|
SecurityOpt: "securityOpt",
|
|
783
949
|
Ulimits: "ulimits",
|
|
784
|
-
Tmpfs: "tmpfs"
|
|
950
|
+
Tmpfs: "tmpfs",
|
|
951
|
+
Mounts: "mounts"
|
|
785
952
|
}[field] ?? String(field);
|
|
786
953
|
}
|
|
787
954
|
//#endregion
|
|
@@ -892,6 +1059,87 @@ const dockerSandboxProvider = {
|
|
|
892
1059
|
type: "object",
|
|
893
1060
|
description: "tmpfs mount paths with options",
|
|
894
1061
|
additionalProperties: { type: "string" }
|
|
1062
|
+
},
|
|
1063
|
+
mounts: {
|
|
1064
|
+
type: "array",
|
|
1065
|
+
description: "Mounts mapped 1:1 onto Docker HostConfig.Mounts (supports volume subpath)",
|
|
1066
|
+
items: { oneOf: [
|
|
1067
|
+
{
|
|
1068
|
+
type: "object",
|
|
1069
|
+
required: [
|
|
1070
|
+
"type",
|
|
1071
|
+
"target",
|
|
1072
|
+
"source"
|
|
1073
|
+
],
|
|
1074
|
+
additionalProperties: false,
|
|
1075
|
+
properties: {
|
|
1076
|
+
type: { const: "volume" },
|
|
1077
|
+
target: { type: "string" },
|
|
1078
|
+
source: { type: "string" },
|
|
1079
|
+
readOnly: { type: "boolean" },
|
|
1080
|
+
volumeOptions: {
|
|
1081
|
+
type: "object",
|
|
1082
|
+
additionalProperties: false,
|
|
1083
|
+
properties: {
|
|
1084
|
+
subpath: { type: "string" },
|
|
1085
|
+
noCopy: { type: "boolean" },
|
|
1086
|
+
labels: {
|
|
1087
|
+
type: "object",
|
|
1088
|
+
additionalProperties: { type: "string" }
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
type: "object",
|
|
1096
|
+
required: [
|
|
1097
|
+
"type",
|
|
1098
|
+
"target",
|
|
1099
|
+
"source"
|
|
1100
|
+
],
|
|
1101
|
+
additionalProperties: false,
|
|
1102
|
+
properties: {
|
|
1103
|
+
type: { const: "bind" },
|
|
1104
|
+
target: { type: "string" },
|
|
1105
|
+
source: { type: "string" },
|
|
1106
|
+
readOnly: { type: "boolean" },
|
|
1107
|
+
bindOptions: {
|
|
1108
|
+
type: "object",
|
|
1109
|
+
additionalProperties: false,
|
|
1110
|
+
properties: { propagation: {
|
|
1111
|
+
type: "string",
|
|
1112
|
+
enum: [
|
|
1113
|
+
"private",
|
|
1114
|
+
"rprivate",
|
|
1115
|
+
"shared",
|
|
1116
|
+
"rshared",
|
|
1117
|
+
"slave",
|
|
1118
|
+
"rslave"
|
|
1119
|
+
]
|
|
1120
|
+
} }
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
type: "object",
|
|
1126
|
+
required: ["type", "target"],
|
|
1127
|
+
additionalProperties: false,
|
|
1128
|
+
properties: {
|
|
1129
|
+
type: { const: "tmpfs" },
|
|
1130
|
+
target: { type: "string" },
|
|
1131
|
+
readOnly: { type: "boolean" },
|
|
1132
|
+
tmpfsOptions: {
|
|
1133
|
+
type: "object",
|
|
1134
|
+
additionalProperties: false,
|
|
1135
|
+
properties: {
|
|
1136
|
+
sizeBytes: { type: "number" },
|
|
1137
|
+
mode: { type: "number" }
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
] }
|
|
895
1143
|
}
|
|
896
1144
|
}
|
|
897
1145
|
},
|