run-spaceapp 0.1.3 → 0.1.6
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 +13 -4
- package/package.json +4 -3
- package/src/cli.mjs +137 -14
- package/src/prerequisites.mjs +815 -0
- package/templates/compose.yml +6 -0
package/README.md
CHANGED
|
@@ -20,10 +20,19 @@ Windows 11 PowerShell:
|
|
|
20
20
|
npm install -g run-spaceapp; if ($LASTEXITCODE -eq 0) { spaceapp install }
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
The launcher requires Node.js 20.11 or newer, 4 CPUs, 8 GB RAM, 15 GiB free
|
|
24
|
-
disk
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
The launcher requires Node.js 20.11 or newer, 4 CPUs, 8 GB RAM, and 15 GiB free
|
|
24
|
+
disk. If Docker is missing, `spaceapp install` automatically installs it from
|
|
25
|
+
official Docker sources on Windows, macOS, Ubuntu, Debian, Fedora, RHEL, and
|
|
26
|
+
CentOS, starts it, and waits for Engine and Compose readiness before pulling
|
|
27
|
+
images. Windows prefers Windows Package Manager's hash-pinned Docker Desktop
|
|
28
|
+
manifest and retains a signed direct-download fallback. Docker Desktop license
|
|
29
|
+
acceptance and Linux `docker` group membership require confirmation inside the
|
|
30
|
+
same command. On Windows, approve any UAC prompt required by WSL2 or Docker
|
|
31
|
+
Desktop. If Windows requests one restart, rerun the same command afterward and
|
|
32
|
+
installation continues safely.
|
|
33
|
+
|
|
34
|
+
The launcher selects the `light` profile below 12 GiB RAM and the `standard`
|
|
35
|
+
profile otherwise; override this with `--profile light` or
|
|
27
36
|
`--profile standard`.
|
|
28
37
|
|
|
29
38
|
Light mode retains every bundled CLI and the core data/workflow services while
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run-spaceapp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Cross-platform Docker launcher for the SpaceApp self-hosted agent workspace",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
31
|
"test": "node --test tests/*.test.mjs",
|
|
32
|
-
"check": "node --check bin/spaceapp.mjs && node --check src/index.mjs && node --check src/cli.mjs",
|
|
32
|
+
"check": "node --check bin/spaceapp.mjs && node --check src/index.mjs && node --check src/cli.mjs && node --check src/prerequisites.mjs",
|
|
33
33
|
"pack:dry-run": "npm pack --dry-run"
|
|
34
34
|
},
|
|
35
35
|
"repository": {
|
|
@@ -44,5 +44,6 @@
|
|
|
44
44
|
"publishConfig": {
|
|
45
45
|
"access": "public",
|
|
46
46
|
"provenance": true
|
|
47
|
-
}
|
|
47
|
+
},
|
|
48
|
+
"gitHead": "81bf4f16666f75cec5b499df28115df3c28e36b8"
|
|
48
49
|
}
|
package/src/cli.mjs
CHANGED
|
@@ -20,15 +20,39 @@ import {
|
|
|
20
20
|
writeRuntimeFiles,
|
|
21
21
|
writeSetupToken
|
|
22
22
|
} from "./index.mjs";
|
|
23
|
+
import {
|
|
24
|
+
ensureDockerAvailable,
|
|
25
|
+
windowsPowerShellArgs
|
|
26
|
+
} from "./prerequisites.mjs";
|
|
27
|
+
|
|
28
|
+
const trustedCommands = new Set([
|
|
29
|
+
"codesign",
|
|
30
|
+
"docker",
|
|
31
|
+
"explorer.exe",
|
|
32
|
+
"hdiutil",
|
|
33
|
+
"open",
|
|
34
|
+
"powershell.exe",
|
|
35
|
+
"sg",
|
|
36
|
+
"spctl",
|
|
37
|
+
"sudo",
|
|
38
|
+
"winget.exe",
|
|
39
|
+
"wsl.exe",
|
|
40
|
+
"xdg-open"
|
|
41
|
+
]);
|
|
42
|
+
const trustedEnvironmentNames = new Set([
|
|
43
|
+
"SPACEAPP_DOCKER_INSTALLER_PATH"
|
|
44
|
+
]);
|
|
23
45
|
|
|
24
46
|
export async function run(argv, {
|
|
25
47
|
env = process.env,
|
|
26
48
|
platform = process.platform,
|
|
49
|
+
arch = process.arch,
|
|
27
50
|
stdout = process.stdout,
|
|
28
51
|
stderr = process.stderr,
|
|
29
52
|
stdin = process.stdin,
|
|
30
53
|
execute = executeCommand,
|
|
31
|
-
inspectResources = inspectSystemResources
|
|
54
|
+
inspectResources = inspectSystemResources,
|
|
55
|
+
ensureDocker = ensureDockerAvailable
|
|
32
56
|
} = {}) {
|
|
33
57
|
const [command = "help", ...args] = argv;
|
|
34
58
|
const root = resolveSpaceAppHome({ env, platform });
|
|
@@ -47,11 +71,14 @@ export async function run(argv, {
|
|
|
47
71
|
root,
|
|
48
72
|
version,
|
|
49
73
|
platform,
|
|
74
|
+
arch,
|
|
75
|
+
env,
|
|
50
76
|
stdin,
|
|
51
77
|
stdout,
|
|
52
78
|
stderr,
|
|
53
79
|
execute,
|
|
54
|
-
inspectResources
|
|
80
|
+
inspectResources,
|
|
81
|
+
ensureDocker
|
|
55
82
|
});
|
|
56
83
|
}
|
|
57
84
|
if (command === "init") {
|
|
@@ -173,11 +200,14 @@ async function installCommand(args, {
|
|
|
173
200
|
root,
|
|
174
201
|
version,
|
|
175
202
|
platform,
|
|
203
|
+
arch,
|
|
204
|
+
env,
|
|
176
205
|
stdin,
|
|
177
206
|
stdout,
|
|
178
207
|
stderr,
|
|
179
208
|
execute,
|
|
180
|
-
inspectResources
|
|
209
|
+
inspectResources,
|
|
210
|
+
ensureDocker
|
|
181
211
|
}) {
|
|
182
212
|
const { requestedProfile, noOpen } = parseInstallArgs(args);
|
|
183
213
|
const resources = await inspectResources(root);
|
|
@@ -193,6 +223,38 @@ async function installCommand(args, {
|
|
|
193
223
|
stdout.write("Store it temporarily; it expires after first owner setup.\n");
|
|
194
224
|
}
|
|
195
225
|
|
|
226
|
+
if (installResourceChecks(resources).some((check) => !check.ok)) {
|
|
227
|
+
const doctorCode = await doctor({
|
|
228
|
+
root,
|
|
229
|
+
platform,
|
|
230
|
+
stdout,
|
|
231
|
+
stderr,
|
|
232
|
+
execute,
|
|
233
|
+
stdin,
|
|
234
|
+
inspectResources,
|
|
235
|
+
resources
|
|
236
|
+
});
|
|
237
|
+
stderr.write("Installation stopped before downloading images. Fix the failed checks and run the same command again.\n");
|
|
238
|
+
return doctorCode;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const prerequisiteResult = await ensureDocker({
|
|
242
|
+
platform,
|
|
243
|
+
arch,
|
|
244
|
+
env,
|
|
245
|
+
stdin,
|
|
246
|
+
stdout,
|
|
247
|
+
stderr,
|
|
248
|
+
execute,
|
|
249
|
+
installArgs: { requestedProfile, noOpen }
|
|
250
|
+
});
|
|
251
|
+
if (prerequisiteResult.reexecuted || prerequisiteResult.code !== 0) {
|
|
252
|
+
if (prerequisiteResult.code !== 0) {
|
|
253
|
+
stderr.write("Installation stopped before downloading images. Fix the failed checks and run the same command again.\n");
|
|
254
|
+
}
|
|
255
|
+
return prerequisiteResult.code;
|
|
256
|
+
}
|
|
257
|
+
|
|
196
258
|
const doctorCode = await doctor({
|
|
197
259
|
root,
|
|
198
260
|
platform,
|
|
@@ -201,7 +263,8 @@ async function installCommand(args, {
|
|
|
201
263
|
execute,
|
|
202
264
|
stdin,
|
|
203
265
|
inspectResources,
|
|
204
|
-
resources
|
|
266
|
+
resources,
|
|
267
|
+
dockerReady: true
|
|
205
268
|
});
|
|
206
269
|
if (doctorCode !== 0) {
|
|
207
270
|
stderr.write("Installation stopped before downloading images. Fix the failed checks and run the same command again.\n");
|
|
@@ -392,7 +455,8 @@ async function doctor({
|
|
|
392
455
|
execute,
|
|
393
456
|
stdin,
|
|
394
457
|
inspectResources,
|
|
395
|
-
resources
|
|
458
|
+
resources,
|
|
459
|
+
dockerReady = false
|
|
396
460
|
}) {
|
|
397
461
|
const detectedResources = resources ?? await inspectResources(root);
|
|
398
462
|
const checks = [
|
|
@@ -403,9 +467,12 @@ async function doctor({
|
|
|
403
467
|
let dockerMissing = false;
|
|
404
468
|
for (const probe of [
|
|
405
469
|
{ name: "Docker", command: "docker", args: ["--version"] },
|
|
406
|
-
{ name: "Docker Compose", command: "docker", args: ["compose", "version"] }
|
|
470
|
+
{ name: "Docker Compose", command: "docker", args: ["compose", "version"] },
|
|
471
|
+
{ name: "Docker Engine", command: "docker", args: ["info"] }
|
|
407
472
|
]) {
|
|
408
|
-
const code =
|
|
473
|
+
const code = dockerReady
|
|
474
|
+
? 0
|
|
475
|
+
: await execute(probe, { stdin, stdout: null, stderr: null });
|
|
409
476
|
if (code !== 0) dockerMissing = true;
|
|
410
477
|
checks.push({ name: probe.name, ok: code === 0, detail: code === 0 ? "available" : "missing" });
|
|
411
478
|
}
|
|
@@ -462,7 +529,29 @@ export async function readSecret(stdin, stdout, prompt, { mask = true } = {}) {
|
|
|
462
529
|
|
|
463
530
|
export function executeCommand(spec, { stdin, stdout, stderr, input } = {}) {
|
|
464
531
|
return new Promise((resolve, reject) => {
|
|
465
|
-
|
|
532
|
+
if (!spec || typeof spec !== "object" || !trustedCommands.has(spec.command)) {
|
|
533
|
+
reject(new Error("SpaceApp refused to execute an untrusted command."));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (spec.command === "powershell.exe" && (
|
|
537
|
+
spec.args !== undefined ||
|
|
538
|
+
typeof spec.operation !== "string"
|
|
539
|
+
)) {
|
|
540
|
+
reject(new Error("SpaceApp PowerShell commands must use a trusted operation."));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (spec.command !== "powershell.exe" && (
|
|
544
|
+
!Array.isArray(spec.args) ||
|
|
545
|
+
spec.args.some((argument) => typeof argument !== "string")
|
|
546
|
+
)) {
|
|
547
|
+
reject(new Error("SpaceApp command arguments must be strings."));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const commandEnv = spec.env === undefined
|
|
551
|
+
? process.env
|
|
552
|
+
: validateCommandEnvironment(spec.env);
|
|
553
|
+
const child = spawnTrustedCommand(spec, {
|
|
554
|
+
env: commandEnv,
|
|
466
555
|
shell: false,
|
|
467
556
|
stdio: [input === undefined ? (stdin || "inherit") : "pipe", stdout || "ignore", stderr || "ignore"]
|
|
468
557
|
});
|
|
@@ -482,12 +571,46 @@ export function executeCommand(spec, { stdin, stdout, stderr, input } = {}) {
|
|
|
482
571
|
});
|
|
483
572
|
}
|
|
484
573
|
|
|
574
|
+
function spawnTrustedCommand(spec, options) {
|
|
575
|
+
const args = spec.args;
|
|
576
|
+
switch (spec.command) {
|
|
577
|
+
case "codesign": return spawn("codesign", args, options);
|
|
578
|
+
case "docker": return spawn("docker", args, options);
|
|
579
|
+
case "explorer.exe": return spawn("explorer.exe", args, options);
|
|
580
|
+
case "hdiutil": return spawn("hdiutil", args, options);
|
|
581
|
+
case "open": return spawn("open", args, options);
|
|
582
|
+
case "powershell.exe":
|
|
583
|
+
return spawn("powershell.exe", windowsPowerShellArgs(spec.operation), options);
|
|
584
|
+
case "sg": return spawn("sg", args, options);
|
|
585
|
+
case "spctl": return spawn("spctl", args, options);
|
|
586
|
+
case "sudo": return spawn("sudo", args, options);
|
|
587
|
+
case "winget.exe": return spawn("winget.exe", args, options);
|
|
588
|
+
case "wsl.exe": return spawn("wsl.exe", args, options);
|
|
589
|
+
case "xdg-open": return spawn("xdg-open", args, options);
|
|
590
|
+
default: throw new Error("SpaceApp refused to execute an untrusted command.");
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function validateCommandEnvironment(commandEnvironment) {
|
|
595
|
+
if (
|
|
596
|
+
!commandEnvironment ||
|
|
597
|
+
typeof commandEnvironment !== "object" ||
|
|
598
|
+
Array.isArray(commandEnvironment) ||
|
|
599
|
+
Object.entries(commandEnvironment).some(
|
|
600
|
+
([name, value]) => !trustedEnvironmentNames.has(name) || typeof value !== "string"
|
|
601
|
+
)
|
|
602
|
+
) {
|
|
603
|
+
throw new Error("SpaceApp refused untrusted command environment values.");
|
|
604
|
+
}
|
|
605
|
+
return { ...process.env, ...commandEnvironment };
|
|
606
|
+
}
|
|
607
|
+
|
|
485
608
|
function openBrowser(url, platform, execute, io) {
|
|
486
609
|
if (platform === "darwin") {
|
|
487
610
|
return execute({ command: "open", args: [url] }, io);
|
|
488
611
|
}
|
|
489
612
|
if (platform === "win32") {
|
|
490
|
-
return execute({ command: "
|
|
613
|
+
return execute({ command: "explorer.exe", args: [url] }, io);
|
|
491
614
|
}
|
|
492
615
|
return execute({ command: "xdg-open", args: [url] }, io);
|
|
493
616
|
}
|
|
@@ -500,12 +623,12 @@ function assertNoArgs(args, command) {
|
|
|
500
623
|
|
|
501
624
|
function dockerInstallHelp(platform) {
|
|
502
625
|
if (platform === "win32") {
|
|
503
|
-
return "
|
|
626
|
+
return 'Run "spaceapp install" to install and start signed Docker Desktop with WSL2 automatically.';
|
|
504
627
|
}
|
|
505
628
|
if (platform === "darwin") {
|
|
506
|
-
return "
|
|
629
|
+
return 'Run "spaceapp install" to install and start signed Docker Desktop automatically.';
|
|
507
630
|
}
|
|
508
|
-
return "
|
|
631
|
+
return 'Run "spaceapp install" to install and start Docker Engine and Compose automatically on supported Linux distributions.';
|
|
509
632
|
}
|
|
510
633
|
|
|
511
634
|
function formatGigabytes(bytes) {
|
|
@@ -524,10 +647,10 @@ Usage: spaceapp <command>
|
|
|
524
647
|
|
|
525
648
|
init Create a local SpaceApp installation
|
|
526
649
|
install [--profile auto|light|standard] [--no-open]
|
|
527
|
-
|
|
650
|
+
Install prerequisites, initialize, and start
|
|
528
651
|
up | down | status | logs Manage the Docker application
|
|
529
652
|
open Open the local web application
|
|
530
|
-
doctor Check
|
|
653
|
+
doctor Check resources, Docker, Compose, and engine
|
|
531
654
|
update [version] | rollback Update or roll back images
|
|
532
655
|
backup | restore Back up or restore persistent state
|
|
533
656
|
workspace add <path> [--read-only]
|
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
|
+
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { delimiter, join, posix, win32 } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { Readable } from "node:stream";
|
|
8
|
+
import { pipeline } from "node:stream/promises";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const DOCKER_TERMS_URL = "https://www.docker.com/legal/docker-subscription-service-agreement/";
|
|
13
|
+
const DOWNLOAD_TIMEOUT_MS = 30 * 60 * 1_000;
|
|
14
|
+
const WINDOWS_DOCKER_DOWNLOADS = Object.freeze({
|
|
15
|
+
x64: "https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe",
|
|
16
|
+
arm64: "https://desktop.docker.com/win/main/arm64/Docker%20Desktop%20Installer.exe"
|
|
17
|
+
});
|
|
18
|
+
const MAC_DOCKER_DOWNLOADS = Object.freeze({
|
|
19
|
+
x64: "https://desktop.docker.com/mac/main/amd64/Docker.dmg",
|
|
20
|
+
arm64: "https://desktop.docker.com/mac/main/arm64/Docker.dmg"
|
|
21
|
+
});
|
|
22
|
+
// Sources:
|
|
23
|
+
// https://learn.microsoft.com/windows/package-manager/winget/install
|
|
24
|
+
// https://github.com/microsoft/winget-pkgs/tree/master/manifests/d/Docker/DockerDesktop
|
|
25
|
+
const WINDOWS_DOCKER_WINGET_ARGS = Object.freeze([
|
|
26
|
+
"install",
|
|
27
|
+
"--id", "Docker.DockerDesktop",
|
|
28
|
+
"--exact",
|
|
29
|
+
"--source", "winget",
|
|
30
|
+
"--silent",
|
|
31
|
+
"--accept-package-agreements",
|
|
32
|
+
"--accept-source-agreements",
|
|
33
|
+
"--disable-interactivity"
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export function windowsPowerShellArgs(operation) {
|
|
37
|
+
const scripts = {
|
|
38
|
+
"install-wsl": [
|
|
39
|
+
"$ErrorActionPreference = 'Stop'; try {",
|
|
40
|
+
"$process = Start-Process -FilePath 'wsl.exe'",
|
|
41
|
+
"-ArgumentList @('--install','--no-distribution')",
|
|
42
|
+
"-Verb RunAs -Wait -PassThru",
|
|
43
|
+
"; exit $process.ExitCode",
|
|
44
|
+
"} catch { exit 1 }"
|
|
45
|
+
].join(" "),
|
|
46
|
+
"verify-docker-installer": [
|
|
47
|
+
"$path = $env:SPACEAPP_DOCKER_INSTALLER_PATH",
|
|
48
|
+
"if ([string]::IsNullOrWhiteSpace($path)) { exit 1 }",
|
|
49
|
+
"$signature = Get-AuthenticodeSignature -LiteralPath $path",
|
|
50
|
+
"if ($signature.Status -ne 'Valid' -or $signature.SignerCertificate.Subject -notmatch 'Docker') { exit 1 }"
|
|
51
|
+
].join("; "),
|
|
52
|
+
"install-docker-desktop": [
|
|
53
|
+
"$path = $env:SPACEAPP_DOCKER_INSTALLER_PATH",
|
|
54
|
+
"if ([string]::IsNullOrWhiteSpace($path)) { exit 1 }",
|
|
55
|
+
"$process = Start-Process -FilePath $path -ArgumentList @('install','--user','--quiet','--accept-license') -Wait -PassThru",
|
|
56
|
+
"exit $process.ExitCode"
|
|
57
|
+
].join("; "),
|
|
58
|
+
"start-docker-desktop": [
|
|
59
|
+
"$path = $env:SPACEAPP_DOCKER_DESKTOP_PATH",
|
|
60
|
+
"if ([string]::IsNullOrWhiteSpace($path)) { exit 1 }",
|
|
61
|
+
"Start-Process -FilePath $path"
|
|
62
|
+
].join("; ")
|
|
63
|
+
};
|
|
64
|
+
if (!Object.hasOwn(scripts, operation)) {
|
|
65
|
+
throw new Error("SpaceApp refused an untrusted PowerShell operation.");
|
|
66
|
+
}
|
|
67
|
+
return ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", scripts[operation]];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function ensureDockerAvailable({
|
|
71
|
+
platform = process.platform,
|
|
72
|
+
arch = process.arch,
|
|
73
|
+
env = process.env,
|
|
74
|
+
stdin = process.stdin,
|
|
75
|
+
stdout = process.stdout,
|
|
76
|
+
stderr = process.stderr,
|
|
77
|
+
execute,
|
|
78
|
+
launch = launchDetachedCommand,
|
|
79
|
+
download = downloadFile,
|
|
80
|
+
pathExists = fileExists,
|
|
81
|
+
sleep = wait,
|
|
82
|
+
osRelease,
|
|
83
|
+
installArgs
|
|
84
|
+
}) {
|
|
85
|
+
if (await dockerIsReady(execute)) {
|
|
86
|
+
return { code: 0, reexecuted: false };
|
|
87
|
+
}
|
|
88
|
+
if (platform === "win32") {
|
|
89
|
+
return ensureWindowsDocker({
|
|
90
|
+
arch,
|
|
91
|
+
env,
|
|
92
|
+
stdin,
|
|
93
|
+
stdout,
|
|
94
|
+
stderr,
|
|
95
|
+
execute,
|
|
96
|
+
launch,
|
|
97
|
+
download,
|
|
98
|
+
pathExists,
|
|
99
|
+
sleep
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (platform === "darwin") {
|
|
103
|
+
return ensureMacDocker({
|
|
104
|
+
arch,
|
|
105
|
+
env,
|
|
106
|
+
stdin,
|
|
107
|
+
stdout,
|
|
108
|
+
stderr,
|
|
109
|
+
execute,
|
|
110
|
+
launch,
|
|
111
|
+
download,
|
|
112
|
+
pathExists,
|
|
113
|
+
sleep
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return ensureLinuxDocker({
|
|
117
|
+
arch,
|
|
118
|
+
env,
|
|
119
|
+
stdin,
|
|
120
|
+
stdout,
|
|
121
|
+
stderr,
|
|
122
|
+
execute,
|
|
123
|
+
download,
|
|
124
|
+
sleep,
|
|
125
|
+
osRelease,
|
|
126
|
+
installArgs
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function ensureWindowsDocker({
|
|
131
|
+
arch,
|
|
132
|
+
env,
|
|
133
|
+
stdin,
|
|
134
|
+
stdout,
|
|
135
|
+
stderr,
|
|
136
|
+
execute,
|
|
137
|
+
launch,
|
|
138
|
+
download,
|
|
139
|
+
pathExists,
|
|
140
|
+
sleep
|
|
141
|
+
}) {
|
|
142
|
+
const paths = windowsDockerPaths(env);
|
|
143
|
+
let application = await firstExisting(paths.applications, pathExists);
|
|
144
|
+
|
|
145
|
+
if (!application) {
|
|
146
|
+
stdout.write("Docker Desktop is required and is not installed.\n");
|
|
147
|
+
const accepted = await confirmDesktopInstall({ platformName: "Windows", stdin, stdout });
|
|
148
|
+
if (!accepted) {
|
|
149
|
+
stderr.write("Docker Desktop installation was cancelled.\n");
|
|
150
|
+
return { code: 1, reexecuted: false };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const wslCode = await execute(
|
|
155
|
+
{ command: "wsl.exe", args: ["--version"] },
|
|
156
|
+
{ stdin, stdout: null, stderr: null }
|
|
157
|
+
);
|
|
158
|
+
if (wslCode !== 0) {
|
|
159
|
+
stdout.write("Installing the Windows Subsystem for Linux 2 support required by Docker Desktop...\n");
|
|
160
|
+
const installWslCode = await execute(
|
|
161
|
+
{
|
|
162
|
+
command: "powershell.exe",
|
|
163
|
+
operation: "install-wsl"
|
|
164
|
+
},
|
|
165
|
+
{ stdin, stdout, stderr }
|
|
166
|
+
);
|
|
167
|
+
if (installWslCode !== 0) {
|
|
168
|
+
stderr.write(
|
|
169
|
+
"Windows could not enable WSL2 automatically. Run this command from an Administrator terminal, restart Windows if requested, and run it again.\n"
|
|
170
|
+
);
|
|
171
|
+
return { code: installWslCode, reexecuted: false };
|
|
172
|
+
}
|
|
173
|
+
const installedWslCode = await execute(
|
|
174
|
+
{ command: "wsl.exe", args: ["--version"] },
|
|
175
|
+
{ stdin, stdout: null, stderr: null }
|
|
176
|
+
);
|
|
177
|
+
if (installedWslCode !== 0) {
|
|
178
|
+
stderr.write(
|
|
179
|
+
"Windows must restart to finish enabling WSL2. Restart Windows, then run the same SpaceApp command again.\n"
|
|
180
|
+
);
|
|
181
|
+
return { code: 1, reexecuted: false };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!application) {
|
|
186
|
+
const wingetCode = await execute(
|
|
187
|
+
{ command: "winget.exe", args: ["--version"] },
|
|
188
|
+
{ stdin, stdout: null, stderr: null }
|
|
189
|
+
);
|
|
190
|
+
if (wingetCode === 0) {
|
|
191
|
+
stdout.write(
|
|
192
|
+
"Installing Docker Desktop through Windows Package Manager with manifest hash verification...\n"
|
|
193
|
+
);
|
|
194
|
+
const installCode = await execute(
|
|
195
|
+
{ command: "winget.exe", args: [...WINDOWS_DOCKER_WINGET_ARGS] },
|
|
196
|
+
{ stdin, stdout, stderr }
|
|
197
|
+
);
|
|
198
|
+
if (installCode === 0) {
|
|
199
|
+
application = await firstExisting(paths.applications, pathExists);
|
|
200
|
+
if (!application) {
|
|
201
|
+
stderr.write(
|
|
202
|
+
"Windows Package Manager reported success, but Docker Desktop could not be found.\n"
|
|
203
|
+
);
|
|
204
|
+
return { code: 1, reexecuted: false };
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
stdout.write(
|
|
208
|
+
"Windows Package Manager could not install Docker Desktop; trying Docker's signed direct installer...\n"
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!application) {
|
|
215
|
+
const downloadUrl = WINDOWS_DOCKER_DOWNLOADS[arch];
|
|
216
|
+
if (!downloadUrl) {
|
|
217
|
+
stderr.write(`Docker Desktop automatic installation does not support Windows architecture "${arch}".\n`);
|
|
218
|
+
return { code: 1, reexecuted: false };
|
|
219
|
+
}
|
|
220
|
+
const temporaryRoot = await mkdtemp(join(tmpdir(), "spaceapp-docker-"));
|
|
221
|
+
const installer = join(temporaryRoot, "Docker Desktop Installer.exe");
|
|
222
|
+
const installerEnv = {
|
|
223
|
+
SPACEAPP_DOCKER_INSTALLER_PATH: installer
|
|
224
|
+
};
|
|
225
|
+
try {
|
|
226
|
+
stdout.write("Downloading Docker Desktop from Docker's official distribution service...\n");
|
|
227
|
+
await download(downloadUrl, installer);
|
|
228
|
+
const signatureCode = await execute({
|
|
229
|
+
command: "powershell.exe",
|
|
230
|
+
operation: "verify-docker-installer",
|
|
231
|
+
env: installerEnv
|
|
232
|
+
}, { stdin, stdout: null, stderr });
|
|
233
|
+
if (signatureCode !== 0) {
|
|
234
|
+
stderr.write("The Docker Desktop installer signature is not valid. Installation was stopped.\n");
|
|
235
|
+
return { code: 1, reexecuted: false };
|
|
236
|
+
}
|
|
237
|
+
const installCode = await execute(
|
|
238
|
+
{
|
|
239
|
+
command: "powershell.exe",
|
|
240
|
+
operation: "install-docker-desktop",
|
|
241
|
+
env: installerEnv
|
|
242
|
+
},
|
|
243
|
+
{ stdin, stdout, stderr }
|
|
244
|
+
);
|
|
245
|
+
if (installCode !== 0) {
|
|
246
|
+
stderr.write("Docker Desktop installation failed before SpaceApp downloaded any images.\n");
|
|
247
|
+
return { code: installCode, reexecuted: false };
|
|
248
|
+
}
|
|
249
|
+
} finally {
|
|
250
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
application = await firstExisting(paths.applications, pathExists);
|
|
253
|
+
if (!application) {
|
|
254
|
+
stderr.write("Docker Desktop reported success, but its application could not be found.\n");
|
|
255
|
+
return { code: 1, reexecuted: false };
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
stdout.write("Docker Desktop is installed but is not running. Starting it now...\n");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const cliDirectory = await firstExisting(paths.cliDirectories, pathExists);
|
|
262
|
+
if (cliDirectory) {
|
|
263
|
+
prependPath(env, cliDirectory);
|
|
264
|
+
}
|
|
265
|
+
const launchCode = await launch({
|
|
266
|
+
operation: "start-docker-desktop",
|
|
267
|
+
env: {
|
|
268
|
+
SPACEAPP_DOCKER_DESKTOP_PATH: application
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
if (launchCode !== 0) {
|
|
272
|
+
stderr.write("Docker Desktop is installed, but SpaceApp could not start it.\n");
|
|
273
|
+
return { code: launchCode, reexecuted: false };
|
|
274
|
+
}
|
|
275
|
+
if (await waitForDocker({ execute, sleep })) {
|
|
276
|
+
stdout.write("Docker Desktop is ready.\n");
|
|
277
|
+
return { code: 0, reexecuted: false };
|
|
278
|
+
}
|
|
279
|
+
stderr.write(
|
|
280
|
+
"Docker Desktop was installed but did not become ready. Complete any Docker Desktop prompt or restart Windows if requested, then run the same SpaceApp command again.\n"
|
|
281
|
+
);
|
|
282
|
+
return { code: 1, reexecuted: false };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function ensureMacDocker({
|
|
286
|
+
arch,
|
|
287
|
+
env,
|
|
288
|
+
stdin,
|
|
289
|
+
stdout,
|
|
290
|
+
stderr,
|
|
291
|
+
execute,
|
|
292
|
+
launch,
|
|
293
|
+
download,
|
|
294
|
+
pathExists,
|
|
295
|
+
sleep
|
|
296
|
+
}) {
|
|
297
|
+
const application = "/Applications/Docker.app";
|
|
298
|
+
const cliDirectory = posix.join(application, "Contents", "Resources", "bin");
|
|
299
|
+
let installed = await pathExists(application);
|
|
300
|
+
|
|
301
|
+
if (!installed) {
|
|
302
|
+
stdout.write("Docker Desktop is required and is not installed.\n");
|
|
303
|
+
const accepted = await confirmDesktopInstall({ platformName: "macOS", stdin, stdout });
|
|
304
|
+
if (!accepted) {
|
|
305
|
+
stderr.write("Docker Desktop installation was cancelled.\n");
|
|
306
|
+
return { code: 1, reexecuted: false };
|
|
307
|
+
}
|
|
308
|
+
const downloadUrl = MAC_DOCKER_DOWNLOADS[arch];
|
|
309
|
+
if (!downloadUrl) {
|
|
310
|
+
stderr.write(`Docker Desktop automatic installation does not support macOS architecture "${arch}".\n`);
|
|
311
|
+
return { code: 1, reexecuted: false };
|
|
312
|
+
}
|
|
313
|
+
const username = String(env.USER || "");
|
|
314
|
+
if (!/^[A-Za-z0-9._-]+$/.test(username)) {
|
|
315
|
+
stderr.write("A valid macOS username is required for Docker Desktop installation.\n");
|
|
316
|
+
return { code: 1, reexecuted: false };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const temporaryRoot = await mkdtemp(join(tmpdir(), "spaceapp-docker-"));
|
|
320
|
+
const diskImage = join(temporaryRoot, "Docker.dmg");
|
|
321
|
+
const mountedApplication = "/Volumes/Docker/Docker.app";
|
|
322
|
+
const installer = posix.join(mountedApplication, "Contents", "MacOS", "install");
|
|
323
|
+
let mounted = false;
|
|
324
|
+
try {
|
|
325
|
+
stdout.write("Downloading Docker Desktop from Docker's official distribution service...\n");
|
|
326
|
+
await download(downloadUrl, diskImage);
|
|
327
|
+
const attachCode = await execute(
|
|
328
|
+
{ command: "hdiutil", args: ["attach", "-nobrowse", diskImage] },
|
|
329
|
+
{ stdin, stdout, stderr }
|
|
330
|
+
);
|
|
331
|
+
if (attachCode !== 0) {
|
|
332
|
+
return { code: attachCode, reexecuted: false };
|
|
333
|
+
}
|
|
334
|
+
mounted = true;
|
|
335
|
+
const signatureCode = await execute({
|
|
336
|
+
command: "codesign",
|
|
337
|
+
args: ["--verify", "--deep", "--strict", "--verbose=2", mountedApplication]
|
|
338
|
+
}, { stdin, stdout: null, stderr });
|
|
339
|
+
if (signatureCode !== 0) {
|
|
340
|
+
stderr.write("The Docker Desktop application signature is not valid. Installation was stopped.\n");
|
|
341
|
+
return { code: 1, reexecuted: false };
|
|
342
|
+
}
|
|
343
|
+
const gatekeeperCode = await execute({
|
|
344
|
+
command: "spctl",
|
|
345
|
+
args: ["--assess", "--type", "execute", "--verbose=4", mountedApplication]
|
|
346
|
+
}, { stdin, stdout: null, stderr });
|
|
347
|
+
if (gatekeeperCode !== 0) {
|
|
348
|
+
stderr.write("macOS Gatekeeper did not accept Docker Desktop. Installation was stopped.\n");
|
|
349
|
+
return { code: 1, reexecuted: false };
|
|
350
|
+
}
|
|
351
|
+
const installCode = await execute({
|
|
352
|
+
command: "sudo",
|
|
353
|
+
args: [installer, "--accept-license", `--user=${username}`]
|
|
354
|
+
}, { stdin, stdout, stderr });
|
|
355
|
+
if (installCode !== 0) {
|
|
356
|
+
stderr.write("Docker Desktop installation failed before SpaceApp downloaded any images.\n");
|
|
357
|
+
return { code: installCode, reexecuted: false };
|
|
358
|
+
}
|
|
359
|
+
} finally {
|
|
360
|
+
if (mounted) {
|
|
361
|
+
await execute(
|
|
362
|
+
{ command: "hdiutil", args: ["detach", "/Volumes/Docker"] },
|
|
363
|
+
{ stdin, stdout: null, stderr: null }
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
367
|
+
}
|
|
368
|
+
installed = await pathExists(application);
|
|
369
|
+
if (!installed) {
|
|
370
|
+
stderr.write("Docker Desktop reported success, but /Applications/Docker.app could not be found.\n");
|
|
371
|
+
return { code: 1, reexecuted: false };
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
stdout.write("Docker Desktop is installed but is not running. Starting it now...\n");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (await pathExists(cliDirectory)) {
|
|
378
|
+
prependPath(env, cliDirectory);
|
|
379
|
+
}
|
|
380
|
+
const launchCode = await launch({ operation: "open-docker-desktop" });
|
|
381
|
+
if (launchCode !== 0) {
|
|
382
|
+
stderr.write("Docker Desktop is installed, but SpaceApp could not start it.\n");
|
|
383
|
+
return { code: launchCode, reexecuted: false };
|
|
384
|
+
}
|
|
385
|
+
if (await waitForDocker({ execute, sleep })) {
|
|
386
|
+
stdout.write("Docker Desktop is ready.\n");
|
|
387
|
+
return { code: 0, reexecuted: false };
|
|
388
|
+
}
|
|
389
|
+
stderr.write(
|
|
390
|
+
"Docker Desktop was installed but did not become ready. Complete any Docker Desktop prompt, then run the same SpaceApp command again.\n"
|
|
391
|
+
);
|
|
392
|
+
return { code: 1, reexecuted: false };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async function ensureLinuxDocker({
|
|
396
|
+
arch,
|
|
397
|
+
env,
|
|
398
|
+
stdin,
|
|
399
|
+
stdout,
|
|
400
|
+
stderr,
|
|
401
|
+
execute,
|
|
402
|
+
download,
|
|
403
|
+
osRelease,
|
|
404
|
+
installArgs
|
|
405
|
+
}) {
|
|
406
|
+
const cliAvailable = await dockerCliAndComposeAreAvailable(execute);
|
|
407
|
+
if (!cliAvailable) {
|
|
408
|
+
const release = osRelease ?? await readLinuxOsRelease();
|
|
409
|
+
stdout.write(`Docker Engine is required and is not installed. Installing it for ${release.ID}...\n`);
|
|
410
|
+
const installCode = await installLinuxDockerEngine({
|
|
411
|
+
arch,
|
|
412
|
+
release,
|
|
413
|
+
stdin,
|
|
414
|
+
stdout,
|
|
415
|
+
stderr,
|
|
416
|
+
execute,
|
|
417
|
+
download
|
|
418
|
+
});
|
|
419
|
+
if (installCode !== 0) {
|
|
420
|
+
return { code: installCode, reexecuted: false };
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
stdout.write("Docker Engine is installed but is not ready. Starting it now...\n");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const startCode = await execute(
|
|
427
|
+
{ command: "sudo", args: ["systemctl", "enable", "--now", "docker"] },
|
|
428
|
+
{ stdin, stdout, stderr }
|
|
429
|
+
);
|
|
430
|
+
if (startCode !== 0) {
|
|
431
|
+
stderr.write("Docker Engine was installed, but its system service could not be started.\n");
|
|
432
|
+
return { code: startCode, reexecuted: false };
|
|
433
|
+
}
|
|
434
|
+
if (await dockerIsReady(execute)) {
|
|
435
|
+
stdout.write("Docker Engine is ready.\n");
|
|
436
|
+
return { code: 0, reexecuted: false };
|
|
437
|
+
}
|
|
438
|
+
if (env.SPACEAPP_PREREQUISITES_BOOTSTRAPPED === "1") {
|
|
439
|
+
stderr.write("Docker Engine is running, but the current user still cannot access it.\n");
|
|
440
|
+
return { code: 1, reexecuted: false };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const username = String(env.SUDO_USER || env.USER || "");
|
|
444
|
+
if (!/^[A-Za-z0-9._-]+$/.test(username) || username === "root") {
|
|
445
|
+
stderr.write("A valid non-root Linux username is required for Docker Engine access.\n");
|
|
446
|
+
return { code: 1, reexecuted: false };
|
|
447
|
+
}
|
|
448
|
+
const accepted = await confirmQuestion({
|
|
449
|
+
stdin,
|
|
450
|
+
stdout,
|
|
451
|
+
question: [
|
|
452
|
+
"Docker's official post-install flow uses the docker group.",
|
|
453
|
+
"Membership grants root-level privileges on this host.",
|
|
454
|
+
`Add ${username} to the docker group and continue? [y/N] `
|
|
455
|
+
].join("\n")
|
|
456
|
+
});
|
|
457
|
+
if (!accepted) {
|
|
458
|
+
stderr.write("Docker group membership was not changed.\n");
|
|
459
|
+
return { code: 1, reexecuted: false };
|
|
460
|
+
}
|
|
461
|
+
const groupCode = await execute(
|
|
462
|
+
{ command: "sudo", args: ["usermod", "-aG", "docker", username] },
|
|
463
|
+
{ stdin, stdout, stderr }
|
|
464
|
+
);
|
|
465
|
+
if (groupCode !== 0) {
|
|
466
|
+
return { code: groupCode, reexecuted: false };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const reentryCommand = linuxReentryCommand(installArgs);
|
|
470
|
+
stdout.write("Docker Engine is ready. Continuing SpaceApp installation with the new group membership...\n");
|
|
471
|
+
const reentryCode = await execute(
|
|
472
|
+
{ command: "sg", args: ["docker", "-c", reentryCommand] },
|
|
473
|
+
{ stdin, stdout, stderr }
|
|
474
|
+
);
|
|
475
|
+
return { code: reentryCode, reexecuted: true };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function confirmDesktopInstall({ platformName, stdin, stdout }) {
|
|
479
|
+
return confirmQuestion({
|
|
480
|
+
stdin,
|
|
481
|
+
stdout,
|
|
482
|
+
question:
|
|
483
|
+
`Review the Docker Desktop terms at ${DOCKER_TERMS_URL}\n` +
|
|
484
|
+
`Install Docker Desktop for ${platformName} and accept those terms? [y/N] `
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function confirmQuestion({ stdin, stdout, question }) {
|
|
489
|
+
const readline = createInterface({ input: stdin, output: stdout, terminal: Boolean(stdin.isTTY) });
|
|
490
|
+
try {
|
|
491
|
+
const answer = await readline.question(question);
|
|
492
|
+
return /^(?:y|yes)$/i.test(answer.trim());
|
|
493
|
+
} catch {
|
|
494
|
+
return false;
|
|
495
|
+
} finally {
|
|
496
|
+
readline.close();
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function dockerCliAndComposeAreAvailable(execute) {
|
|
501
|
+
for (const spec of [
|
|
502
|
+
{ command: "docker", args: ["--version"] },
|
|
503
|
+
{ command: "docker", args: ["compose", "version"] }
|
|
504
|
+
]) {
|
|
505
|
+
if (await execute(spec, { stdout: null, stderr: null }) !== 0) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function dockerIsReady(execute) {
|
|
513
|
+
for (const spec of [
|
|
514
|
+
{ command: "docker", args: ["--version"] },
|
|
515
|
+
{ command: "docker", args: ["compose", "version"] },
|
|
516
|
+
{ command: "docker", args: ["info"] }
|
|
517
|
+
]) {
|
|
518
|
+
if (await execute(spec, { stdout: null, stderr: null }) !== 0) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function installLinuxDockerEngine({
|
|
526
|
+
arch,
|
|
527
|
+
release,
|
|
528
|
+
stdin,
|
|
529
|
+
stdout,
|
|
530
|
+
stderr,
|
|
531
|
+
execute,
|
|
532
|
+
download
|
|
533
|
+
}) {
|
|
534
|
+
const id = String(release?.ID || "").toLowerCase();
|
|
535
|
+
if (id === "ubuntu" || id === "debian") {
|
|
536
|
+
return installAptDockerEngine({
|
|
537
|
+
arch,
|
|
538
|
+
id,
|
|
539
|
+
codename: release.VERSION_CODENAME || release.UBUNTU_CODENAME,
|
|
540
|
+
stdin,
|
|
541
|
+
stdout,
|
|
542
|
+
stderr,
|
|
543
|
+
execute,
|
|
544
|
+
download
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
if (id === "fedora" || id === "rhel" || id === "centos") {
|
|
548
|
+
return installDnfDockerEngine({ id, stdin, stdout, stderr, execute });
|
|
549
|
+
}
|
|
550
|
+
stderr.write(
|
|
551
|
+
`Automatic Docker Engine installation is not yet supported for Linux distribution "${id || "unknown"}". ` +
|
|
552
|
+
"Install Docker Engine and the Compose plugin from https://docs.docker.com/engine/install/ and rerun the same command.\n"
|
|
553
|
+
);
|
|
554
|
+
return 1;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function installAptDockerEngine({
|
|
558
|
+
arch,
|
|
559
|
+
id,
|
|
560
|
+
codename,
|
|
561
|
+
stdin,
|
|
562
|
+
stdout,
|
|
563
|
+
stderr,
|
|
564
|
+
execute,
|
|
565
|
+
download
|
|
566
|
+
}) {
|
|
567
|
+
const dockerArch = { x64: "amd64", arm64: "arm64" }[arch];
|
|
568
|
+
if (!dockerArch || !/^[a-z0-9._-]+$/.test(String(codename || ""))) {
|
|
569
|
+
stderr.write("This Debian/Ubuntu architecture or release codename is not supported automatically.\n");
|
|
570
|
+
return 1;
|
|
571
|
+
}
|
|
572
|
+
const temporaryRoot = await mkdtemp(join(tmpdir(), "spaceapp-docker-"));
|
|
573
|
+
const signingKey = join(temporaryRoot, "docker.asc");
|
|
574
|
+
const repositoryFile = join(temporaryRoot, "docker.list");
|
|
575
|
+
const repositoryBase = `https://download.docker.com/linux/${id}`;
|
|
576
|
+
try {
|
|
577
|
+
await download(`${repositoryBase}/gpg`, signingKey);
|
|
578
|
+
await writeFile(
|
|
579
|
+
repositoryFile,
|
|
580
|
+
`deb [arch=${dockerArch} signed-by=/etc/apt/keyrings/docker.asc] ${repositoryBase} ${codename} stable\n`,
|
|
581
|
+
{ mode: 0o600, flag: "wx" }
|
|
582
|
+
);
|
|
583
|
+
for (const spec of [
|
|
584
|
+
{ command: "sudo", args: ["install", "-m", "0755", "-d", "/etc/apt/keyrings"] },
|
|
585
|
+
{ command: "sudo", args: ["install", "-m", "0644", signingKey, "/etc/apt/keyrings/docker.asc"] },
|
|
586
|
+
{
|
|
587
|
+
command: "sudo",
|
|
588
|
+
args: ["install", "-m", "0644", repositoryFile, "/etc/apt/sources.list.d/docker.list"]
|
|
589
|
+
},
|
|
590
|
+
{ command: "sudo", args: ["apt-get", "update"] },
|
|
591
|
+
{
|
|
592
|
+
command: "sudo",
|
|
593
|
+
args: [
|
|
594
|
+
"apt-get",
|
|
595
|
+
"install",
|
|
596
|
+
"-y",
|
|
597
|
+
"docker-ce",
|
|
598
|
+
"docker-ce-cli",
|
|
599
|
+
"containerd.io",
|
|
600
|
+
"docker-buildx-plugin",
|
|
601
|
+
"docker-compose-plugin"
|
|
602
|
+
]
|
|
603
|
+
}
|
|
604
|
+
]) {
|
|
605
|
+
const code = await execute(spec, { stdin, stdout, stderr });
|
|
606
|
+
if (code !== 0) {
|
|
607
|
+
return code;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return 0;
|
|
611
|
+
} finally {
|
|
612
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function installDnfDockerEngine({ id, stdin, stdout, stderr, execute }) {
|
|
617
|
+
const repositoryUrl = `https://download.docker.com/linux/${id}/docker-ce.repo`;
|
|
618
|
+
const repositoryArgs = id === "fedora"
|
|
619
|
+
? ["dnf", "config-manager", "addrepo", "--from-repofile", repositoryUrl]
|
|
620
|
+
: ["dnf", "config-manager", "--add-repo", repositoryUrl];
|
|
621
|
+
const pluginCode = await execute(
|
|
622
|
+
{ command: "sudo", args: ["dnf", "-y", "install", "dnf-plugins-core"] },
|
|
623
|
+
{ stdin, stdout, stderr }
|
|
624
|
+
);
|
|
625
|
+
if (pluginCode !== 0) {
|
|
626
|
+
return pluginCode;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
let repositoryCode = await execute(
|
|
630
|
+
{ command: "sudo", args: repositoryArgs },
|
|
631
|
+
{ stdin, stdout, stderr }
|
|
632
|
+
);
|
|
633
|
+
if (repositoryCode !== 0 && id === "fedora") {
|
|
634
|
+
stdout.write("The DNF5 repository command was unavailable; retrying with DNF4 syntax...\n");
|
|
635
|
+
repositoryCode = await execute(
|
|
636
|
+
{
|
|
637
|
+
command: "sudo",
|
|
638
|
+
args: ["dnf", "config-manager", "--add-repo", repositoryUrl]
|
|
639
|
+
},
|
|
640
|
+
{ stdin, stdout, stderr }
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
if (repositoryCode !== 0) {
|
|
644
|
+
return repositoryCode;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return execute(
|
|
648
|
+
{
|
|
649
|
+
command: "sudo",
|
|
650
|
+
args: [
|
|
651
|
+
"dnf",
|
|
652
|
+
"-y",
|
|
653
|
+
"install",
|
|
654
|
+
"docker-ce",
|
|
655
|
+
"docker-ce-cli",
|
|
656
|
+
"containerd.io",
|
|
657
|
+
"docker-buildx-plugin",
|
|
658
|
+
"docker-compose-plugin"
|
|
659
|
+
]
|
|
660
|
+
},
|
|
661
|
+
{ stdin, stdout, stderr }
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function readLinuxOsRelease() {
|
|
666
|
+
const content = await readFile("/etc/os-release", "utf8");
|
|
667
|
+
const values = {};
|
|
668
|
+
for (const line of content.split(/\r?\n/)) {
|
|
669
|
+
const match = /^([A-Z_]+)=(.*)$/.exec(line);
|
|
670
|
+
if (!match) continue;
|
|
671
|
+
const raw = match[2].trim();
|
|
672
|
+
values[match[1]] = raw.replace(/^(['"])(.*)\1$/, "$2");
|
|
673
|
+
}
|
|
674
|
+
return values;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function linuxReentryCommand({ requestedProfile = "auto", noOpen = false } = {}) {
|
|
678
|
+
if (!["auto", "light", "standard"].includes(requestedProfile)) {
|
|
679
|
+
throw new Error("Invalid SpaceApp profile for Docker group re-entry.");
|
|
680
|
+
}
|
|
681
|
+
const entrypoint = fileURLToPath(new URL("../bin/spaceapp.mjs", import.meta.url));
|
|
682
|
+
return [
|
|
683
|
+
"/usr/bin/env",
|
|
684
|
+
"SPACEAPP_PREREQUISITES_BOOTSTRAPPED=1",
|
|
685
|
+
process.execPath,
|
|
686
|
+
entrypoint,
|
|
687
|
+
"install",
|
|
688
|
+
"--profile",
|
|
689
|
+
requestedProfile,
|
|
690
|
+
...(noOpen ? ["--no-open"] : [])
|
|
691
|
+
].map(shellQuote).join(" ");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function shellQuote(value) {
|
|
695
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function waitForDocker({ execute, sleep }) {
|
|
699
|
+
for (let attempt = 0; attempt < 90; attempt += 1) {
|
|
700
|
+
if (await dockerIsReady(execute)) {
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
await sleep(2_000);
|
|
704
|
+
}
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function windowsDockerPaths(env) {
|
|
709
|
+
const localAppData = env.LOCALAPPDATA;
|
|
710
|
+
const programFiles = env.ProgramFiles || env.PROGRAMFILES;
|
|
711
|
+
return {
|
|
712
|
+
applications: [
|
|
713
|
+
localAppData && win32.join(localAppData, "Programs", "DockerDesktop", "Docker Desktop.exe"),
|
|
714
|
+
programFiles && win32.join(programFiles, "Docker", "Docker", "Docker Desktop.exe")
|
|
715
|
+
].filter(Boolean),
|
|
716
|
+
cliDirectories: [
|
|
717
|
+
localAppData && win32.join(localAppData, "Programs", "DockerDesktop", "resources", "bin"),
|
|
718
|
+
programFiles && win32.join(programFiles, "Docker", "Docker", "resources", "bin")
|
|
719
|
+
].filter(Boolean)
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function firstExisting(paths, pathExists) {
|
|
724
|
+
for (const path of paths) {
|
|
725
|
+
if (await pathExists(path)) {
|
|
726
|
+
return path;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function prependPath(env, directory) {
|
|
733
|
+
const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") || "PATH";
|
|
734
|
+
const entries = String(env[pathKey] || "").split(delimiter).filter(Boolean);
|
|
735
|
+
if (!entries.includes(directory)) {
|
|
736
|
+
env[pathKey] = [directory, ...entries].join(delimiter);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async function fileExists(path) {
|
|
741
|
+
try {
|
|
742
|
+
await access(path);
|
|
743
|
+
return true;
|
|
744
|
+
} catch {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export async function downloadFile(url, target, {
|
|
750
|
+
timeoutMs = DOWNLOAD_TIMEOUT_MS
|
|
751
|
+
} = {}) {
|
|
752
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
753
|
+
throw new Error("Docker download timeout must be a positive number.");
|
|
754
|
+
}
|
|
755
|
+
const controller = new AbortController();
|
|
756
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
757
|
+
try {
|
|
758
|
+
const response = await fetch(url, {
|
|
759
|
+
redirect: "follow",
|
|
760
|
+
signal: controller.signal
|
|
761
|
+
});
|
|
762
|
+
if (!response.ok || !response.body) {
|
|
763
|
+
throw new Error(`Docker download failed with HTTP ${response.status}.`);
|
|
764
|
+
}
|
|
765
|
+
await pipeline(
|
|
766
|
+
Readable.fromWeb(response.body),
|
|
767
|
+
createWriteStream(target, { flags: "wx", mode: 0o600 }),
|
|
768
|
+
{ signal: controller.signal }
|
|
769
|
+
);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
if (controller.signal.aborted) {
|
|
772
|
+
throw new Error(
|
|
773
|
+
`Docker download timed out after ${Math.ceil(timeoutMs / 1_000)} seconds.`,
|
|
774
|
+
{ cause: error }
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
throw error;
|
|
778
|
+
} finally {
|
|
779
|
+
clearTimeout(timeout);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function launchDetachedCommand(spec) {
|
|
784
|
+
return new Promise((resolve) => {
|
|
785
|
+
const options = {
|
|
786
|
+
detached: true,
|
|
787
|
+
env: spec.env ? { ...process.env, ...spec.env } : process.env,
|
|
788
|
+
shell: false,
|
|
789
|
+
stdio: "ignore"
|
|
790
|
+
};
|
|
791
|
+
let child;
|
|
792
|
+
try {
|
|
793
|
+
if (spec.operation === "start-docker-desktop") {
|
|
794
|
+
child = spawn("powershell.exe", windowsPowerShellArgs(spec.operation), options);
|
|
795
|
+
} else if (spec.operation === "open-docker-desktop") {
|
|
796
|
+
child = spawn("open", ["-a", "Docker"], options);
|
|
797
|
+
} else {
|
|
798
|
+
resolve(1);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
} catch {
|
|
802
|
+
resolve(1);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
child.once("error", () => resolve(1));
|
|
806
|
+
child.once("spawn", () => {
|
|
807
|
+
child.unref();
|
|
808
|
+
resolve(0);
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function wait(milliseconds) {
|
|
814
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
815
|
+
}
|
package/templates/compose.yml
CHANGED
|
@@ -3,6 +3,8 @@ services:
|
|
|
3
3
|
image: ghcr.io/oll4com/spaceapp-core:${SPACEAPP_IMAGE_TAG}
|
|
4
4
|
restart: unless-stopped
|
|
5
5
|
init: true
|
|
6
|
+
security_opt:
|
|
7
|
+
- no-new-privileges:true
|
|
6
8
|
mem_limit: "${SPACEAPP_CORE_MEMORY_LIMIT}"
|
|
7
9
|
cpus: "${SPACEAPP_CORE_CPU_LIMIT}"
|
|
8
10
|
environment:
|
|
@@ -70,6 +72,8 @@ services:
|
|
|
70
72
|
image: ghcr.io/oll4com/spaceapp-cli:${SPACEAPP_IMAGE_TAG}
|
|
71
73
|
restart: unless-stopped
|
|
72
74
|
init: true
|
|
75
|
+
security_opt:
|
|
76
|
+
- no-new-privileges:true
|
|
73
77
|
mem_limit: "${SPACEAPP_CLI_MEMORY_LIMIT}"
|
|
74
78
|
cpus: "${SPACEAPP_CLI_CPU_LIMIT}"
|
|
75
79
|
environment:
|
|
@@ -90,6 +94,8 @@ services:
|
|
|
90
94
|
profiles: ["standard"]
|
|
91
95
|
restart: unless-stopped
|
|
92
96
|
init: true
|
|
97
|
+
security_opt:
|
|
98
|
+
- no-new-privileges:true
|
|
93
99
|
mem_limit: "${SPACEAPP_BROWSER_MEMORY_LIMIT}"
|
|
94
100
|
cpus: "${SPACEAPP_BROWSER_CPU_LIMIT}"
|
|
95
101
|
environment:
|