mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
docs(deploy): document Colima build swap helper (#967)
* docs(deploy): document Colima build swap helper Explain when Apple Silicon Colima users should prepare temporary VM swap before manual image publishing, and cover the host guard with a focused test. * fix(deploy): harden Colima swap helper Address review feedback by validating swap overrides, passing remote shell values safely, preserving configured fallback sizes, and expanding behavior coverage.
This commit is contained in:
parent
ebe3513ed4
commit
34b5b85614
3 changed files with 625 additions and 0 deletions
|
|
@ -64,3 +64,48 @@ The script defaults to:
|
||||||
If `127.0.0.1:7890` is available and no proxy is already set, the script uses it
|
If `127.0.0.1:7890` is available and no proxy is already set, the script uses it
|
||||||
for registry access and passes `host.docker.internal:7890` into Docker builds. The
|
for registry access and passes `host.docker.internal:7890` into Docker builds. The
|
||||||
host-gateway alias is only added for builds that need this local proxy mapping.
|
host-gateway alias is only added for builds that need this local proxy mapping.
|
||||||
|
|
||||||
|
### Colima swap helper for Apple Silicon
|
||||||
|
|
||||||
|
`deploy/scripts/prepare-colima-build-swap.sh` is for manual Docker image
|
||||||
|
publishing from an Apple Silicon macOS host that uses Colima as the Docker VM.
|
||||||
|
The helper is intentionally Apple Silicon-only because the failure mode it covers
|
||||||
|
is local arm64 Colima builds exhausting a small Linux VM while preparing
|
||||||
|
multi-arch images. It exits before touching Colima on non-macOS or
|
||||||
|
non-Apple-Silicon hosts.
|
||||||
|
|
||||||
|
Low-memory Colima VMs can run out of RAM during multi-arch image builds. The
|
||||||
|
helper checks the VM memory and swap status, then creates and enables a temporary
|
||||||
|
swap file only when the VM has no swap and less than 4 GiB of RAM. The 4 GiB
|
||||||
|
threshold is a conservative default for short-lived manual publishes on small
|
||||||
|
Colima profiles; raise `COLIMA_BUILD_SWAP_MEMORY_THRESHOLD_KIB` if larger builds
|
||||||
|
still OOM, or lower it if you only want swap for very small VMs.
|
||||||
|
|
||||||
|
Prefer increasing the Colima VM memory (`colima start --memory <GiB>` or the
|
||||||
|
profile config) when you want a persistent build machine. Use this helper when
|
||||||
|
you need a temporary, reversible boost for one manual publish without resizing
|
||||||
|
or recreating the VM.
|
||||||
|
|
||||||
|
Run it before a manual publish if Docker builds fail with out-of-memory errors,
|
||||||
|
or if `status` shows a small Colima VM with no swap. The swap remains active
|
||||||
|
until cleanup or VM restart, so use a shell trap for one-off sessions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deploy/scripts/prepare-colima-build-swap.sh status
|
||||||
|
deploy/scripts/prepare-colima-build-swap.sh
|
||||||
|
trap 'deploy/scripts/prepare-colima-build-swap.sh cleanup' EXIT
|
||||||
|
deploy/scripts/publish-images.sh --image_tag latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful overrides:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
COLIMA_BUILD_SWAP_SIZE=6G deploy/scripts/prepare-colima-build-swap.sh
|
||||||
|
COLIMA_BUILD_SWAP_MEMORY_THRESHOLD_KIB=6291456 deploy/scripts/prepare-colima-build-swap.sh
|
||||||
|
COLIMA_BIN=/opt/homebrew/bin/colima deploy/scripts/prepare-colima-build-swap.sh status
|
||||||
|
COLIMA_BUILD_SWAP_CLEANUP_FORCE=1 COLIMA_BUILD_SWAPFILE=/custom-swapfile deploy/scripts/prepare-colima-build-swap.sh cleanup
|
||||||
|
```
|
||||||
|
|
||||||
|
`cleanup` removes the default helper path and the old helper path. If you set a
|
||||||
|
custom `COLIMA_BUILD_SWAPFILE`, cleanup refuses to remove it unless
|
||||||
|
`COLIMA_BUILD_SWAP_CLEANUP_FORCE=1` is also set.
|
||||||
|
|
|
||||||
199
deploy/scripts/prepare-colima-build-swap.sh
Executable file
199
deploy/scripts/prepare-colima-build-swap.sh
Executable file
|
|
@ -0,0 +1,199 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ACTION="${1:-ensure}"
|
||||||
|
SWAP_SIZE="${COLIMA_BUILD_SWAP_SIZE:-4G}"
|
||||||
|
SWAP_FILE="${COLIMA_BUILD_SWAPFILE:-/swapfile-colima-build}"
|
||||||
|
DEFAULT_SWAP_FILE="/swapfile-colima-build"
|
||||||
|
LEGACY_SWAP_FILE="/swapfile-open-design-build"
|
||||||
|
MEMORY_THRESHOLD_KIB="${COLIMA_BUILD_SWAP_MEMORY_THRESHOLD_KIB:-4194304}"
|
||||||
|
CLEANUP_FORCE="${COLIMA_BUILD_SWAP_CLEANUP_FORCE:-0}"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[prepare-colima-build-swap] %s\n' "$*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf '[prepare-colima-build-swap] ERROR: %s\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
host_os() {
|
||||||
|
uname -s
|
||||||
|
}
|
||||||
|
|
||||||
|
host_arch() {
|
||||||
|
uname -m
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_config() {
|
||||||
|
[[ "$SWAP_SIZE" =~ ^[1-9][0-9]*[GgMm]$ ]] || die "COLIMA_BUILD_SWAP_SIZE must be an integer size ending in G or M"
|
||||||
|
[[ "$SWAP_FILE" =~ ^/[A-Za-z0-9._/-]+$ ]] || die "COLIMA_BUILD_SWAPFILE must be an absolute path using only letters, digits, dot, underscore, dash, and slash"
|
||||||
|
[[ "$MEMORY_THRESHOLD_KIB" =~ ^[0-9]+$ ]] || die "COLIMA_BUILD_SWAP_MEMORY_THRESHOLD_KIB must be an integer KiB value"
|
||||||
|
[[ "$CLEANUP_FORCE" == "0" || "$CLEANUP_FORCE" == "1" ]] || die "COLIMA_BUILD_SWAP_CLEANUP_FORCE must be 0 or 1"
|
||||||
|
}
|
||||||
|
|
||||||
|
swap_size_mib() {
|
||||||
|
local size_value="${SWAP_SIZE%[GgMm]}"
|
||||||
|
local size_unit="${SWAP_SIZE: -1}"
|
||||||
|
|
||||||
|
case "$size_unit" in
|
||||||
|
G|g)
|
||||||
|
printf '%s' $((10#$size_value * 1024))
|
||||||
|
;;
|
||||||
|
M|m)
|
||||||
|
printf '%s' $((10#$size_value))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
require_apple_silicon_macos() {
|
||||||
|
[[ "$(host_os)" == "Darwin" && "$(host_arch)" == "arm64" ]] || die "prepare-colima-build-swap requires Apple Silicon macOS"
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_colima_bin() {
|
||||||
|
if [[ -n "${COLIMA_BIN:-}" ]]; then
|
||||||
|
printf '%s' "$COLIMA_BIN"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$(host_arch)" == "arm64" && -x /opt/homebrew/bin/colima ]]; then
|
||||||
|
printf '%s' /opt/homebrew/bin/colima
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v colima 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
prepare-colima-build-swap.sh [ensure|status|cleanup]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
ensure Create and enable a Colima VM swap file if memory is low. Default.
|
||||||
|
status Print Colima VM memory/swap status.
|
||||||
|
cleanup Disable and remove the swap file created by this script.
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
COLIMA_BUILD_SWAP_SIZE=4G
|
||||||
|
COLIMA_BUILD_SWAPFILE=/swapfile-colima-build
|
||||||
|
COLIMA_BUILD_SWAP_MEMORY_THRESHOLD_KIB=4194304
|
||||||
|
COLIMA_BUILD_SWAP_CLEANUP_FORCE=0
|
||||||
|
COLIMA_BIN=/opt/homebrew/bin/colima
|
||||||
|
|
||||||
|
Run this before manual multi-arch Docker publishing on Apple Silicon Colima:
|
||||||
|
prepare-colima-build-swap.sh
|
||||||
|
<your docker buildx / image publish command>
|
||||||
|
prepare-colima-build-swap.sh cleanup
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
require_colima() {
|
||||||
|
local colima_bin="$1"
|
||||||
|
[[ -n "$colima_bin" ]] || die "colima not found; install Colima or set COLIMA_BIN"
|
||||||
|
"$colima_bin" status >/dev/null || die "Colima is not running"
|
||||||
|
}
|
||||||
|
|
||||||
|
vm_status() {
|
||||||
|
local colima_bin="$1"
|
||||||
|
"$colima_bin" ssh -- sh -lc '
|
||||||
|
set -e
|
||||||
|
awk "/^MemTotal:|^SwapTotal:/ { print }" /proc/meminfo
|
||||||
|
swapon --show || true
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_swap() {
|
||||||
|
local colima_bin="$1"
|
||||||
|
local memory_total_kib
|
||||||
|
local swap_total_kib
|
||||||
|
|
||||||
|
memory_total_kib="$("$colima_bin" ssh -- awk '/^MemTotal:/ { print $2; exit }' /proc/meminfo)"
|
||||||
|
swap_total_kib="$("$colima_bin" ssh -- awk '/^SwapTotal:/ { print $2; exit }' /proc/meminfo)"
|
||||||
|
|
||||||
|
[[ "$memory_total_kib" =~ ^[0-9]+$ ]] || die "failed to read Colima MemTotal"
|
||||||
|
[[ "$swap_total_kib" =~ ^[0-9]+$ ]] || die "failed to read Colima SwapTotal"
|
||||||
|
|
||||||
|
if (( 10#$swap_total_kib > 0 )); then
|
||||||
|
log "Colima already has swap (${swap_total_kib}KiB); no changes made"
|
||||||
|
vm_status "$colima_bin"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( 10#$memory_total_kib >= 10#$MEMORY_THRESHOLD_KIB )); then
|
||||||
|
log "Colima memory is ${memory_total_kib}KiB, at or above threshold ${MEMORY_THRESHOLD_KIB}KiB; no swap needed"
|
||||||
|
vm_status "$colima_bin"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local swap_size_mib_value
|
||||||
|
swap_size_mib_value="$(swap_size_mib)"
|
||||||
|
|
||||||
|
log "enabling $SWAP_SIZE swap at $SWAP_FILE for low-memory Colima (${memory_total_kib}KiB RAM)"
|
||||||
|
"$colima_bin" ssh -- sh -lc '
|
||||||
|
set -e
|
||||||
|
swapfile="$1"
|
||||||
|
swap_size="$2"
|
||||||
|
swap_size_mib="$3"
|
||||||
|
if ! swapon --show=NAME --noheadings | grep -qx "$swapfile"; then
|
||||||
|
if [ ! -f "$swapfile" ]; then
|
||||||
|
sudo fallocate -l "$swap_size" "$swapfile" 2>/dev/null || sudo dd if=/dev/zero of="$swapfile" bs=1M count="$swap_size_mib" status=progress
|
||||||
|
sudo chmod 600 "$swapfile"
|
||||||
|
sudo mkswap "$swapfile" >/dev/null
|
||||||
|
fi
|
||||||
|
sudo swapon "$swapfile"
|
||||||
|
fi
|
||||||
|
' sh "$SWAP_FILE" "$SWAP_SIZE" "$swap_size_mib_value"
|
||||||
|
vm_status "$colima_bin"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_swap() {
|
||||||
|
local colima_bin="$1"
|
||||||
|
|
||||||
|
if [[ "$SWAP_FILE" != "$DEFAULT_SWAP_FILE" && "$CLEANUP_FORCE" != "1" ]]; then
|
||||||
|
die "refusing to cleanup custom swap path $SWAP_FILE; set COLIMA_BUILD_SWAP_CLEANUP_FORCE=1 to remove it"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "removing swap file $SWAP_FILE from Colima if present"
|
||||||
|
"$colima_bin" ssh -- sh -lc '
|
||||||
|
set -e
|
||||||
|
for swapfile in "$@"; do
|
||||||
|
if swapon --show=NAME --noheadings | grep -qx "$swapfile"; then
|
||||||
|
sudo swapoff "$swapfile"
|
||||||
|
fi
|
||||||
|
sudo rm -f "$swapfile"
|
||||||
|
done
|
||||||
|
' sh "$SWAP_FILE" "$LEGACY_SWAP_FILE"
|
||||||
|
vm_status "$colima_bin"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
-h|--help|help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
ensure|status|cleanup)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage >&2
|
||||||
|
die "unknown command: $ACTION"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
validate_config
|
||||||
|
require_apple_silicon_macos
|
||||||
|
COLIMA_BIN_RESOLVED="$(resolve_colima_bin)"
|
||||||
|
require_colima "$COLIMA_BIN_RESOLVED"
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
ensure)
|
||||||
|
ensure_swap "$COLIMA_BIN_RESOLVED"
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
vm_status "$COLIMA_BIN_RESOLVED"
|
||||||
|
;;
|
||||||
|
cleanup)
|
||||||
|
cleanup_swap "$COLIMA_BIN_RESOLVED"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
381
deploy/tests/prepare-colima-build-swap.test.ts
Normal file
381
deploy/tests/prepare-colima-build-swap.test.ts
Normal file
|
|
@ -0,0 +1,381 @@
|
||||||
|
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
const repoRoot = path.resolve(import.meta.dirname, "../..");
|
||||||
|
const scriptPath = path.join(repoRoot, "deploy/scripts/prepare-colima-build-swap.sh");
|
||||||
|
|
||||||
|
type FakeHost = {
|
||||||
|
os: string;
|
||||||
|
arch: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VmState = {
|
||||||
|
memTotalKiB?: number;
|
||||||
|
swapTotalKiB?: number;
|
||||||
|
fallocateFails?: boolean;
|
||||||
|
activeSwapFiles?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type RunOptions = {
|
||||||
|
args?: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
host?: FakeHost;
|
||||||
|
vm?: VmState;
|
||||||
|
};
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runScript({
|
||||||
|
args = ["status"],
|
||||||
|
env = {},
|
||||||
|
host = { os: "Darwin", arch: "arm64" },
|
||||||
|
vm = {},
|
||||||
|
}: RunOptions = {}) {
|
||||||
|
const tempDir = await mkdtemp(path.join(tmpdir(), "prepare-colima-build-swap-"));
|
||||||
|
const binDir = path.join(tempDir, "bin");
|
||||||
|
const remoteBinDir = path.join(tempDir, "remote-bin");
|
||||||
|
await mkdir(binDir);
|
||||||
|
await mkdir(remoteBinDir);
|
||||||
|
|
||||||
|
const statePath = path.join(tempDir, "state.env");
|
||||||
|
const commandLogPath = path.join(tempDir, "commands.log");
|
||||||
|
await writeFile(
|
||||||
|
statePath,
|
||||||
|
[
|
||||||
|
`MEM_TOTAL_KIB=${vm.memTotalKiB ?? 2097152}`,
|
||||||
|
`SWAP_TOTAL_KIB=${vm.swapTotalKiB ?? 0}`,
|
||||||
|
`FALLOCATE_FAILS=${vm.fallocateFails ? "1" : "0"}`,
|
||||||
|
`ACTIVE_SWAP=${(vm.activeSwapFiles ?? []).join(":")}`,
|
||||||
|
"DD_COUNT=",
|
||||||
|
"FALLOCATE_SIZE=",
|
||||||
|
"MKS_SWAP=",
|
||||||
|
"SWAPON_TARGET=",
|
||||||
|
"SWAPOFF_TARGETS=",
|
||||||
|
"RM_TARGETS=",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
path.join(binDir, "uname"),
|
||||||
|
[
|
||||||
|
"#!/usr/bin/env bash",
|
||||||
|
"case \"$1\" in",
|
||||||
|
` -s) printf '%s\\n' ${shellQuote(host.os)} ;;`,
|
||||||
|
` -m) printf '%s\\n' ${shellQuote(host.arch)} ;;`,
|
||||||
|
" *) exit 2 ;;",
|
||||||
|
"esac",
|
||||||
|
].join("\n"),
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
path.join(remoteBinDir, "awk"),
|
||||||
|
[
|
||||||
|
"#!/usr/bin/env bash",
|
||||||
|
"set -euo pipefail",
|
||||||
|
`source ${shellQuote(statePath)}`,
|
||||||
|
"query=\"$*\"",
|
||||||
|
"case \"$query\" in",
|
||||||
|
" *MemTotal*SwapTotal*)",
|
||||||
|
" printf 'MemTotal: %s kB\\n' \"$MEM_TOTAL_KIB\"",
|
||||||
|
" printf 'SwapTotal: %s kB\\n' \"$SWAP_TOTAL_KIB\"",
|
||||||
|
" ;;",
|
||||||
|
" *MemTotal*) printf '%s\\n' \"$MEM_TOTAL_KIB\" ;;",
|
||||||
|
" *SwapTotal*) printf '%s\\n' \"$SWAP_TOTAL_KIB\" ;;",
|
||||||
|
" *) exit 2 ;;",
|
||||||
|
"esac",
|
||||||
|
].join("\n"),
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
path.join(remoteBinDir, "swapon"),
|
||||||
|
[
|
||||||
|
"#!/usr/bin/env bash",
|
||||||
|
"set -euo pipefail",
|
||||||
|
`source ${shellQuote(statePath)}`,
|
||||||
|
"case \"${1:-}\" in",
|
||||||
|
" --show|--show=NAME)",
|
||||||
|
" old_ifs=\"$IFS\"",
|
||||||
|
" IFS=:",
|
||||||
|
" for item in $ACTIVE_SWAP; do",
|
||||||
|
" [ -n \"$item\" ] && printf '%s\\n' \"$item\"",
|
||||||
|
" done",
|
||||||
|
" IFS=\"$old_ifs\"",
|
||||||
|
" ;;",
|
||||||
|
" *)",
|
||||||
|
" printf 'unexpected swapon args: %s\\n' \"$*\" >&2",
|
||||||
|
" exit 2",
|
||||||
|
" ;;",
|
||||||
|
"esac",
|
||||||
|
].join("\n"),
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
path.join(remoteBinDir, "sudo"),
|
||||||
|
[
|
||||||
|
"#!/usr/bin/env bash",
|
||||||
|
"set -euo pipefail",
|
||||||
|
`state=${shellQuote(statePath)}`,
|
||||||
|
"set_state() {",
|
||||||
|
" key=\"$1\"",
|
||||||
|
" value=\"$2\"",
|
||||||
|
" tmp=\"$state.tmp\"",
|
||||||
|
" /usr/bin/awk -F= -v key=\"$key\" -v value=\"$value\" 'BEGIN { done=0 } $1 == key { print key \"=\" value; done=1; next } { print } END { if (!done) print key \"=\" value }' \"$state\" > \"$tmp\"",
|
||||||
|
" mv \"$tmp\" \"$state\"",
|
||||||
|
"}",
|
||||||
|
"append_state() {",
|
||||||
|
" key=\"$1\"",
|
||||||
|
" value=\"$2\"",
|
||||||
|
" current=\"$(/usr/bin/awk -F= -v key=\"$key\" '$1 == key { print substr($0, length(key) + 2); exit }' \"$state\")\"",
|
||||||
|
" if [ -n \"$current\" ]; then",
|
||||||
|
" set_state \"$key\" \"$current:$value\"",
|
||||||
|
" else",
|
||||||
|
" set_state \"$key\" \"$value\"",
|
||||||
|
" fi",
|
||||||
|
"}",
|
||||||
|
"source \"$state\"",
|
||||||
|
"case \"$1\" in",
|
||||||
|
" fallocate)",
|
||||||
|
" if [ \"$FALLOCATE_FAILS\" = 1 ]; then",
|
||||||
|
" exit 1",
|
||||||
|
" fi",
|
||||||
|
" set_state FALLOCATE_SIZE \"$3\"",
|
||||||
|
" ;;",
|
||||||
|
" dd)",
|
||||||
|
" count=\"\"",
|
||||||
|
" for arg in \"$@\"; do",
|
||||||
|
" case \"$arg\" in",
|
||||||
|
" count=*) count=\"${arg#count=}\" ;;",
|
||||||
|
" esac",
|
||||||
|
" done",
|
||||||
|
" set_state DD_COUNT \"$count\"",
|
||||||
|
" ;;",
|
||||||
|
" chmod)",
|
||||||
|
" ;;",
|
||||||
|
" mkswap)",
|
||||||
|
" set_state MKS_SWAP \"$2\"",
|
||||||
|
" ;;",
|
||||||
|
" swapon)",
|
||||||
|
" set_state SWAPON_TARGET \"$2\"",
|
||||||
|
" set_state ACTIVE_SWAP \"$2\"",
|
||||||
|
" set_state SWAP_TOTAL_KIB 1",
|
||||||
|
" ;;",
|
||||||
|
" swapoff)",
|
||||||
|
" append_state SWAPOFF_TARGETS \"$2\"",
|
||||||
|
" set_state ACTIVE_SWAP \"\"",
|
||||||
|
" set_state SWAP_TOTAL_KIB 0",
|
||||||
|
" ;;",
|
||||||
|
" rm)",
|
||||||
|
" shift",
|
||||||
|
" [ \"${1:-}\" = -f ] && shift",
|
||||||
|
" for target in \"$@\"; do",
|
||||||
|
" append_state RM_TARGETS \"$target\"",
|
||||||
|
" done",
|
||||||
|
" ;;",
|
||||||
|
" *)",
|
||||||
|
" printf 'unexpected sudo command: %s\\n' \"$*\" >&2",
|
||||||
|
" exit 2",
|
||||||
|
" ;;",
|
||||||
|
"esac",
|
||||||
|
].join("\n"),
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
path.join(binDir, "colima"),
|
||||||
|
[
|
||||||
|
"#!/usr/bin/env bash",
|
||||||
|
"set -euo pipefail",
|
||||||
|
`log=${shellQuote(commandLogPath)}`,
|
||||||
|
`remote_bin=${shellQuote(remoteBinDir)}`,
|
||||||
|
"printf '%s\\n' \"$*\" >> \"$log\"",
|
||||||
|
"if [ \"$1\" = status ]; then",
|
||||||
|
" exit 0",
|
||||||
|
"fi",
|
||||||
|
"if [ \"$1\" != ssh ]; then",
|
||||||
|
" exit 2",
|
||||||
|
"fi",
|
||||||
|
"shift",
|
||||||
|
"if [ \"${1:-}\" = -- ]; then",
|
||||||
|
" shift",
|
||||||
|
"fi",
|
||||||
|
"case \"$1\" in",
|
||||||
|
" awk)",
|
||||||
|
" PATH=\"$remote_bin:$PATH\" \"$@\"",
|
||||||
|
" ;;",
|
||||||
|
" sh)",
|
||||||
|
" shift",
|
||||||
|
" [ \"$1\" = -lc ] || exit 2",
|
||||||
|
" shift",
|
||||||
|
" script=\"$1\"",
|
||||||
|
" shift",
|
||||||
|
" env PATH=\"$remote_bin:$PATH\" bash -c \"$script\" \"$@\"",
|
||||||
|
" ;;",
|
||||||
|
" *)",
|
||||||
|
" exit 2",
|
||||||
|
" ;;",
|
||||||
|
"esac",
|
||||||
|
].join("\n"),
|
||||||
|
{ mode: 0o755 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const processResult = await new Promise<{ code: number | null; stderr: string }>((resolve, reject) => {
|
||||||
|
const child = spawn("bash", [scriptPath, ...args], {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...env,
|
||||||
|
COLIMA_BIN: path.join(binDir, "colima"),
|
||||||
|
PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`,
|
||||||
|
},
|
||||||
|
stdio: ["ignore", "ignore", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
let stderr = "";
|
||||||
|
child.stderr.setEncoding("utf8");
|
||||||
|
child.stderr.on("data", (chunk) => {
|
||||||
|
stderr += chunk;
|
||||||
|
});
|
||||||
|
child.on("error", reject);
|
||||||
|
child.on("close", (code) => {
|
||||||
|
resolve({ code, stderr });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let colimaLog = "";
|
||||||
|
try {
|
||||||
|
colimaLog = await readFile(commandLogPath, "utf8");
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = Object.fromEntries(
|
||||||
|
(await readFile(statePath, "utf8"))
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => {
|
||||||
|
const [key, ...value] = line.split("=");
|
||||||
|
return [key, value.join("=")];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...processResult, colimaLog, state };
|
||||||
|
}
|
||||||
|
|
||||||
|
test("prepare-colima-build-swap refuses Linux hosts before checking Colima", async () => {
|
||||||
|
const result = await runScript({ args: ["status"], host: { os: "Linux", arch: "x86_64" } });
|
||||||
|
|
||||||
|
assert.equal(result.code, 1);
|
||||||
|
assert.match(result.stderr, /requires Apple Silicon macOS/);
|
||||||
|
assert.equal(result.colimaLog, "");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prepare-colima-build-swap refuses Intel macOS hosts before checking Colima", async () => {
|
||||||
|
const result = await runScript({ args: ["status"], host: { os: "Darwin", arch: "x86_64" } });
|
||||||
|
|
||||||
|
assert.equal(result.code, 1);
|
||||||
|
assert.match(result.stderr, /requires Apple Silicon macOS/);
|
||||||
|
assert.equal(result.colimaLog, "");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prepare-colima-build-swap allows Apple Silicon macOS hosts to check Colima", async () => {
|
||||||
|
const result = await runScript({ args: ["status"] });
|
||||||
|
|
||||||
|
assert.equal(result.code, 0);
|
||||||
|
assert.match(result.colimaLog, /^status\nssh -- sh -lc/m);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ensure creates and enables swap for low-memory Colima without swap", async () => {
|
||||||
|
const result = await runScript({ args: ["ensure"] });
|
||||||
|
|
||||||
|
assert.equal(result.code, 0);
|
||||||
|
assert.equal(result.state.FALLOCATE_SIZE, "4G");
|
||||||
|
assert.equal(result.state.MKS_SWAP, "/swapfile-colima-build");
|
||||||
|
assert.equal(result.state.SWAPON_TARGET, "/swapfile-colima-build");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ensure uses the configured swap size when dd fallback is needed", async () => {
|
||||||
|
const result = await runScript({
|
||||||
|
args: ["ensure"],
|
||||||
|
env: { COLIMA_BUILD_SWAP_SIZE: "6G" },
|
||||||
|
vm: { fallocateFails: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.code, 0);
|
||||||
|
assert.equal(result.state.DD_COUNT, "6144");
|
||||||
|
assert.equal(result.state.SWAPON_TARGET, "/swapfile-colima-build");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ensure is a no-op when Colima already has swap", async () => {
|
||||||
|
const result = await runScript({ args: ["ensure"], vm: { swapTotalKiB: 1048576 } });
|
||||||
|
|
||||||
|
assert.equal(result.code, 0);
|
||||||
|
assert.equal(result.state.FALLOCATE_SIZE, "");
|
||||||
|
assert.equal(result.state.SWAPON_TARGET, "");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ensure is a no-op when Colima memory is above the threshold", async () => {
|
||||||
|
const result = await runScript({ args: ["ensure"], vm: { memTotalKiB: 8388608 } });
|
||||||
|
|
||||||
|
assert.equal(result.code, 0);
|
||||||
|
assert.equal(result.state.FALLOCATE_SIZE, "");
|
||||||
|
assert.equal(result.state.SWAPON_TARGET, "");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cleanup removes the default and legacy swap paths", async () => {
|
||||||
|
const result = await runScript({
|
||||||
|
args: ["cleanup"],
|
||||||
|
vm: { activeSwapFiles: ["/swapfile-colima-build"], swapTotalKiB: 1048576 },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.code, 0);
|
||||||
|
assert.equal(result.state.SWAPOFF_TARGETS, "/swapfile-colima-build");
|
||||||
|
assert.equal(result.state.RM_TARGETS, "/swapfile-colima-build:/swapfile-open-design-build");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cleanup refuses custom swap paths unless force is enabled", async () => {
|
||||||
|
const result = await runScript({
|
||||||
|
args: ["cleanup"],
|
||||||
|
env: { COLIMA_BUILD_SWAPFILE: "/custom-swapfile" },
|
||||||
|
vm: { activeSwapFiles: ["/custom-swapfile"], swapTotalKiB: 1048576 },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.code, 1);
|
||||||
|
assert.match(result.stderr, /refusing to cleanup custom swap path/);
|
||||||
|
assert.equal(result.state.RM_TARGETS, "");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cleanup removes custom swap paths when force is enabled", async () => {
|
||||||
|
const result = await runScript({
|
||||||
|
args: ["cleanup"],
|
||||||
|
env: { COLIMA_BUILD_SWAPFILE: "/custom-swapfile", COLIMA_BUILD_SWAP_CLEANUP_FORCE: "1" },
|
||||||
|
vm: { activeSwapFiles: ["/custom-swapfile"], swapTotalKiB: 1048576 },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.code, 0);
|
||||||
|
assert.equal(result.state.SWAPOFF_TARGETS, "/custom-swapfile");
|
||||||
|
assert.equal(result.state.RM_TARGETS, "/custom-swapfile:/swapfile-open-design-build");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid swap size overrides fail before checking Colima", async () => {
|
||||||
|
const result = await runScript({ args: ["status"], env: { COLIMA_BUILD_SWAP_SIZE: "4G; touch /tmp/pwned" } });
|
||||||
|
|
||||||
|
assert.equal(result.code, 1);
|
||||||
|
assert.match(result.stderr, /COLIMA_BUILD_SWAP_SIZE must be/);
|
||||||
|
assert.equal(result.colimaLog, "");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("invalid swap file overrides fail before checking Colima", async () => {
|
||||||
|
const result = await runScript({ args: ["status"], env: { COLIMA_BUILD_SWAPFILE: "/tmp/swap'file" } });
|
||||||
|
|
||||||
|
assert.equal(result.code, 1);
|
||||||
|
assert.match(result.stderr, /COLIMA_BUILD_SWAPFILE must be/);
|
||||||
|
assert.equal(result.colimaLog, "");
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue