diff --git a/.changeset/fix-docker-installer-asset-fetch.md b/.changeset/fix-docker-installer-asset-fetch.md new file mode 100644 index 0000000..4186488 --- /dev/null +++ b/.changeset/fix-docker-installer-asset-fetch.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Fix the Docker runtime installer so one-line installs can fetch Docker assets into a fresh install directory. diff --git a/docker/install.sh b/docker/install.sh index 128e398..8823893 100755 --- a/docker/install.sh +++ b/docker/install.sh @@ -231,12 +231,14 @@ dotenv_quote() { } fetch_url() { - url=$1 - target=$2 + # POSIX sh function variables are global, so keep these names distinct + # from caller state such as write_asset's target path. + fetch_url_source=$1 + fetch_url_output=$2 if command -v curl >/dev/null 2>&1; then - curl -fsSL "$url" -o "$target" + curl -fsSL "$fetch_url_source" -o "$fetch_url_output" elif command -v wget >/dev/null 2>&1; then - wget -qO "$target" "$url" + wget -qO "$fetch_url_output" "$fetch_url_source" else die "curl or wget is required to fetch Docker assets" fi diff --git a/src/server/dockerControlAssets.test.ts b/src/server/dockerControlAssets.test.ts index 912bc18..35f1965 100644 --- a/src/server/dockerControlAssets.test.ts +++ b/src/server/dockerControlAssets.test.ts @@ -82,6 +82,37 @@ describe("Docker command assets", () => { expect(devCompose).toContain("COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME:-pi-web-dev}"); }); + dockerCommandIt("fetches remote installer assets without clobbering the write target", async () => { + const installDir = join(tempDir, "remote-runtime"); + const fakeDocker = await installFakeDocker(); + await installFakeCurl(fakeDocker.binDir); + await installFakeUname(fakeDocker.binDir, "Darwin"); + const home = join(tempDir, "home"); + const socketPath = join(home, ".docker", "run", "docker.sock"); + + await withUnixSocket(socketPath, async () => { + await execUtf8("sh", [ + join(repoRoot, "docker", "install.sh"), + "--install-dir", installDir, + "--data-dir", join(installDir, "data"), + "--asset-ref", "test-assets", + "--skip-compose", + ], { + ...cleanProcessEnv(), + PATH: `${fakeDocker.binDir}:${process.env["PATH"] ?? ""}`, + HOME: home, + FAKE_DOCKER_LOG: fakeDocker.logPath, + PI_WEB_DOCKER_ASSET_BASE: "https://assets.example.test/docker", + }); + }); + + expect(await readFile(join(installDir, "Dockerfile"), "utf8")).toContain("COPY pi-web-docker /usr/local/bin/pi-web-docker"); + expect(await readFile(join(installDir, "pi-web-docker"), "utf8")).toContain("Usage: pi-web-docker"); + const env = await readFile(join(installDir, ".env"), "utf8"); + expect(env).toContain(`PI_WEB_DOCKER_INSTALL_DIR=${installDir}`); + expect(env).toContain("PI_WEB_DOCKER_REF=test-assets"); + }); + dockerCommandIt("runs status through Docker Compose in the foreground", async () => { const installDir = await createRuntimeInstall(); const fakeDocker = await installFakeDocker(); @@ -492,6 +523,43 @@ exit 9 return { binDir, logPath }; } +async function installFakeCurl(binDir: string): Promise { + const curlPath = join(binDir, "curl"); + await writeFile(curlPath, `#!/usr/bin/env sh +set -eu +asset_root=${shellSingleQuote(join(repoRoot, "docker"))} +output= +url= +while [ "$#" -gt 0 ]; do + case "$1" in + -o) + shift + output=\${1:-} + ;; + -*) + ;; + *) + url=$1 + ;; + esac + if [ "$#" -gt 0 ]; then + shift + fi +done +[ -n "$url" ] || { printf '%s\n' "fake curl missing URL" >&2; exit 2; } +[ -n "$output" ] || { printf '%s\n' "fake curl missing -o output" >&2; exit 2; } +case "$url" in + */docker/*) rel=\${url##*/docker/} ;; + *) printf 'unexpected fake curl url: %s\n' "$url" >&2; exit 2 ;; +esac +src=$asset_root/$rel +[ -f "$src" ] || { printf 'missing fake curl asset: %s\n' "$src" >&2; exit 2; } +mkdir -p "$(dirname "$output")" +cp "$src" "$output" +`, "utf8"); + await chmod(curlPath, 0o755); +} + async function installFakeUname(binDir: string, osName: string): Promise { const unamePath = join(binDir, "uname"); await writeFile(unamePath, `#!/usr/bin/env sh