mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
298 lines
12 KiB
Bash
298 lines
12 KiB
Bash
#!/usr/bin/env bash
|
|
# Exercise the host Fleet CLI against an official cell image on disposable CI.
|
|
set -euo pipefail
|
|
umask 022
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
SOURCE_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$ROOT_DIR}"
|
|
source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"
|
|
if [[ "${GITHUB_ACTIONS:-}" != true || -f /.dockerenv ]]; then
|
|
echo "Fleet Docker proof requires a disposable GitHub Actions Linux host." >&2
|
|
exit 1
|
|
fi
|
|
if ! node "$ROOT_DIR/scripts/e2e/lib/fleet-cache/runtime-preflight.mjs" 2>/dev/null; then
|
|
printf '%s\n' '{"kind":"fleet-runtime-prerequisites","reportCompleted":false,"runtimeControlsExecuted":false}'
|
|
fi
|
|
endpoint="${DOCKER_HOST:-$(docker context inspect --format '{{.Endpoints.docker.Host}}')}"
|
|
[[ "$endpoint" == unix:///* ]]
|
|
socket="${endpoint#unix://}"
|
|
[[ -S "$socket" ]]
|
|
security_options="$(docker info --format '{{json .SecurityOptions}}')"
|
|
printf 'Fleet Docker security options: %s\n' "$security_options"
|
|
jq -e 'all(.[]; contains("rootless") | not)' <<< "$security_options"
|
|
docker version --format '{{.Server.Version}}'
|
|
socket_gid="$(stat -c %g "$socket")"
|
|
node_bin="$(command -v node)"
|
|
node_version="$("$node_bin" -p process.versions.node)"
|
|
|
|
scratch="$(mktemp -d /tmp/openclaw-fleet-cache-e2e.XXXXXX)"
|
|
chmod 755 "$scratch"
|
|
PACKAGE_TGZ=""
|
|
cli_entry="$scratch/host-runtime/node_modules/openclaw/openclaw.mjs"
|
|
tenant=""
|
|
case_dir=""
|
|
uid=1501
|
|
interrupted=false
|
|
|
|
prepare_install_prefix() {
|
|
mkdir -p "$1"
|
|
# npm project installs need lifecycle approval for the exact runtime and package.
|
|
node -e '
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const [prefix, ...specs] = process.argv.slice(1);
|
|
fs.writeFileSync(path.join(prefix, "package.json"), JSON.stringify({
|
|
private: true,
|
|
allowScripts: Object.fromEntries(specs.map((spec) => [spec, true])),
|
|
}));
|
|
' "$@"
|
|
}
|
|
|
|
fleet() (
|
|
cd "$scratch"
|
|
exec timeout --foreground 180s sudo -n setpriv --reuid="$uid" --regid="$uid" --groups="$socket_gid" \
|
|
env -i PATH="$PATH" HOME="$case_dir/home" OPENCLAW_HOME="$case_dir/home" \
|
|
OPENCLAW_STATE_DIR="$case_dir/state" XDG_CACHE_HOME="$case_dir/host-cache" \
|
|
DOCKER_HOST="$endpoint" "$node_bin" "$cli_entry" fleet "$@"
|
|
)
|
|
|
|
capture() {
|
|
local stage="$1"
|
|
echo "==> Fleet $tenant $stage"
|
|
fleet status "$tenant" --json
|
|
fleet logs "$tenant"
|
|
docker inspect "openclaw-cell-$tenant" | jq '.[0] | {
|
|
Id, State, Image, user: .Config.User,
|
|
environment: [.Config.Env[] | select(startswith("OPENCLAW_GATEWAY_TOKEN=") | not)],
|
|
labels: .Config.Labels, mounts: .Mounts,
|
|
resources: {memory: .HostConfig.Memory, cpus: .HostConfig.NanoCpus},
|
|
capDrop: .HostConfig.CapDrop, securityOpt: .HostConfig.SecurityOpt,
|
|
networkMode: .HostConfig.NetworkMode, ports: .HostConfig.PortBindings
|
|
}'
|
|
}
|
|
|
|
cleanup_cell() {
|
|
if [[ -z "$tenant" ]]; then
|
|
return 0
|
|
fi
|
|
fleet rm "$tenant" --force --purge-data
|
|
if docker container inspect "openclaw-cell-$tenant" >/dev/null 2>&1; then
|
|
echo "Fleet proof left a cell container behind: $tenant" >&2
|
|
return 1
|
|
fi
|
|
if docker network inspect "openclaw-cell-$tenant-net" >/dev/null 2>&1; then
|
|
echo "Fleet proof left a cell network behind: $tenant" >&2
|
|
return 1
|
|
fi
|
|
echo "Fleet proof removed cell and network: $tenant"
|
|
tenant=""
|
|
}
|
|
|
|
cleanup() {
|
|
local result=$?
|
|
trap - EXIT
|
|
if (( result != 0 )) && [[ -n "$tenant" && "$interrupted" == false ]]; then
|
|
capture failure || echo "Fleet failure diagnostics were incomplete for $tenant." >&2
|
|
fi
|
|
if ! cleanup_cell; then
|
|
echo "Fleet proof cleanup failed; retained scratch: $scratch" >&2
|
|
exit 1
|
|
fi
|
|
sudo -n rm -rf -- "$scratch"
|
|
[[ ! -e "$scratch" ]]
|
|
echo "Fleet proof removed its scratch directory."
|
|
docker_e2e_cleanup_package_tgz "$PACKAGE_TGZ"
|
|
exit "$result"
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'interrupted=true; exit 130' INT
|
|
trap 'interrupted=true; exit 143' TERM
|
|
|
|
assert_cell() {
|
|
local expected_cache="$1"
|
|
local expected_keys="$2"
|
|
local expected_tmp="$3"
|
|
docker inspect "openclaw-cell-$tenant" | \
|
|
"$node_bin" "$ROOT_DIR/scripts/e2e/lib/fleet-cache/assert-cell.mjs" \
|
|
"$expected_cache" "$expected_keys" "$expected_tmp" "$case_dir/state" "$tenant"
|
|
local expected_uid="$uid"
|
|
if (( uid == 0 )); then
|
|
expected_uid=1000
|
|
fi
|
|
docker exec "openclaw-cell-$tenant" node -e '
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const cache = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache");
|
|
const cacheOwnerUid = fs.statSync(cache).uid;
|
|
const imageHomeOwnerUid = fs.statSync("/home/node").uid;
|
|
assert.equal(process.getuid(), Number(process.argv[1]));
|
|
assert.equal(cacheOwnerUid, process.getuid());
|
|
assert.equal(imageHomeOwnerUid, 1000);
|
|
const cacheProbe = fs.mkdtempSync(path.join(cache, "write-probe-"));
|
|
const tempProbe = fs.mkdtempSync(path.join(os.tmpdir(), "fleet-write-probe-"));
|
|
for (const directory of [cacheProbe, tempProbe]) {
|
|
fs.writeFileSync(path.join(directory, "probe"), "writable");
|
|
fs.rmSync(directory, { recursive: true });
|
|
}
|
|
console.log(JSON.stringify({uid: process.getuid(), gid: process.getgid(),
|
|
imageHomeOwnerUid, cacheOwnerUid, cache, tmpdir: os.tmpdir(),
|
|
cacheWrite: true, temporaryWrite: true}));
|
|
' "$expected_uid"
|
|
}
|
|
|
|
run_case() {
|
|
local name="$1"
|
|
uid="$2"
|
|
local expected_cache="$3"
|
|
local expected_keys="$4"
|
|
local expected_tmp="$5"
|
|
shift 5
|
|
tenant="fleet-cache-${scratch##*.}-$name"
|
|
tenant="$(printf '%s' "$tenant" | tr '[:upper:]' '[:lower:]')"
|
|
case_dir="$scratch/$name"
|
|
mkdir -p "$case_dir/home" "$case_dir/state" "$case_dir/host-cache"
|
|
if [[ -n "$expected_tmp" ]]; then
|
|
mkdir -p "$case_dir/state/fleet/cells/$tenant/operator-tmp"
|
|
fi
|
|
sudo -n chown -R "$uid:$uid" "$case_dir"
|
|
printf 'Fleet invoking UID: %s\n' "$(sudo -n setpriv --reuid="$uid" --regid="$uid" --groups="$socket_gid" id -u)"
|
|
local create_result=0
|
|
fleet create "$tenant" --image "$image" --gateway-token fleet-cache-synthetic-token \
|
|
"$@" --json > "$scratch/create.json" || create_result=$?
|
|
capture create
|
|
if (( create_result != 0 )); then
|
|
echo "Fleet create failed for $name (host UID $uid): $create_result" >&2
|
|
return "$create_result"
|
|
fi
|
|
assert_cell "$expected_cache" "$expected_keys" "$expected_tmp"
|
|
fleet upgrade "$tenant" --image "$image"
|
|
capture upgrade
|
|
assert_cell "$expected_cache" "$expected_keys" "$expected_tmp"
|
|
fleet stop "$tenant"
|
|
fleet backup "$tenant" --out "$case_dir/backup.tgz" --json
|
|
fleet restore "$tenant" --from "$case_dir/backup.tgz" --json > "$scratch/restore.json"
|
|
jq -e '.started == false' "$scratch/restore.json" >/dev/null
|
|
fleet start "$tenant"
|
|
local port
|
|
port="$(docker inspect "openclaw-cell-$tenant" --format '{{(index (index .NetworkSettings.Ports "18789/tcp") 0).HostPort}}')"
|
|
local deadline=$((SECONDS + 60))
|
|
until curl --fail --silent --max-time 1 "http://127.0.0.1:$port/healthz"; do
|
|
if (( SECONDS >= deadline )); then
|
|
capture restore-timeout
|
|
return 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
capture restore
|
|
assert_cell "$expected_cache" "$expected_keys" "$expected_tmp"
|
|
fleet restore "$tenant" --from "$case_dir/backup.tgz" --force --json > "$scratch/restore.json"
|
|
jq -e '.started == true' "$scratch/restore.json" >/dev/null
|
|
capture forced-restore
|
|
assert_cell "$expected_cache" "$expected_keys" "$expected_tmp"
|
|
cleanup_cell
|
|
}
|
|
|
|
run_previous_default_case() {
|
|
local ownership="$1"
|
|
local operation="$2"
|
|
local cache=""
|
|
local keys=""
|
|
local expected_cache=/home/node/.openclaw/cache
|
|
local -a overrides=()
|
|
if [[ "$ownership" == explicit ]]; then
|
|
cache=/home/node/.cache
|
|
keys=XDG_CACHE_HOME
|
|
expected_cache="$cache"
|
|
overrides=(--env "XDG_CACHE_HOME=$cache")
|
|
fi
|
|
uid=1000
|
|
tenant="fleet-cache-${scratch##*.}-$ownership-$operation"
|
|
tenant="$(printf '%s' "$tenant" | tr '[:upper:]' '[:lower:]')"
|
|
case_dir="$scratch/$ownership-$operation"
|
|
mkdir -p "$case_dir/home" "$case_dir/state" "$case_dir/host-cache"
|
|
sudo -n chown -R "$uid:$uid" "$case_dir"
|
|
printf 'Fleet invoking UID: %s\n' "$(sudo -n setpriv --reuid="$uid" --regid="$uid" --groups="$socket_gid" id -u)"
|
|
# The released CLI inherits the image's cache default. Test the upgrade from
|
|
# that real default with both generated and equal-valued explicit input.
|
|
local candidate_entry="$cli_entry"
|
|
cli_entry="$scratch/previous-runtime/node_modules/openclaw/openclaw.mjs"
|
|
fleet create "$tenant" --image "$image" --gateway-token fleet-cache-synthetic-token \
|
|
"${overrides[@]}" --json
|
|
capture previous-create
|
|
assert_cell "$cache" "$keys" ''
|
|
if [[ "$operation" == restore ]]; then
|
|
fleet stop "$tenant"
|
|
fleet backup "$tenant" --out "$case_dir/backup.tgz" --json
|
|
fleet start "$tenant"
|
|
fi
|
|
cli_entry="$candidate_entry"
|
|
if [[ "$operation" == upgrade ]]; then
|
|
fleet upgrade "$tenant" --image "$image"
|
|
else
|
|
fleet restore "$tenant" --from "$case_dir/backup.tgz" --force --json > "$scratch/restore.json"
|
|
jq -e '.started == true' "$scratch/restore.json" >/dev/null
|
|
fi
|
|
capture "previous-default-$operation"
|
|
assert_cell "$expected_cache" "$keys" ''
|
|
cleanup_cell
|
|
}
|
|
|
|
# Neither the runner's toolchain nor its checkout is a public install. Use the
|
|
# canonical package and selected Node version in an accessible test-owned prefix.
|
|
PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz fleet-cache "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")"
|
|
sha256sum "$PACKAGE_TGZ"
|
|
prepare_install_prefix "$scratch/host-runtime" "node@$node_version" "$PACKAGE_TGZ"
|
|
timeout --foreground 600s npm install --prefix "$scratch/host-runtime" --no-save --no-package-lock \
|
|
--no-audit --no-fund "node@$node_version" smol-toml@1.8.0 "$PACKAGE_TGZ"
|
|
node_bin="$scratch/host-runtime/node_modules/node/bin/node"
|
|
export PATH="$scratch/host-runtime/node_modules/.bin:$PATH"
|
|
sudo -n setpriv --reuid=1501 --regid=1501 --groups="$socket_gid" \
|
|
env -i PATH="$PATH" "$node_bin" --version
|
|
source_sha="$(git -C "$SOURCE_ROOT" rev-parse HEAD)"
|
|
"$node_bin" -e '
|
|
const assert = require("node:assert/strict");
|
|
const info = require(process.argv[1]);
|
|
assert.equal(info.commit, process.argv[2]);
|
|
console.log(JSON.stringify(info));
|
|
' "$scratch/host-runtime/node_modules/openclaw/dist/build-info.json" "$source_sha"
|
|
umask 077
|
|
case_dir="$scratch/bootstrap"
|
|
mkdir -p "$case_dir/home" "$case_dir/state" "$case_dir/host-cache"
|
|
sudo -n chown -R "$uid:$uid" "$case_dir"
|
|
fleet --help
|
|
|
|
image_tag="ghcr.io/openclaw/openclaw:2026.8.2"
|
|
timeout --foreground 600s docker pull "$image_tag"
|
|
image="$(docker image inspect "$image_tag" --format '{{index .RepoDigests 0}}')"
|
|
printf 'Fleet host source: %s\nCell image: %s\n' "$source_sha" "$image"
|
|
|
|
run_case mismatch 1501 /home/node/.openclaw/cache '' ''
|
|
run_case override 1501 /home/node/.openclaw/operator-cache XDG_CACHE_HOME '' \
|
|
--env XDG_CACHE_HOME=/home/node/.openclaw/operator-cache
|
|
run_case equal 1501 /home/node/.openclaw/cache XDG_CACHE_HOME '' \
|
|
--env XDG_CACHE_HOME=/home/node/.openclaw/cache
|
|
run_case temporary 1501 /home/node/.openclaw/cache TMPDIR /home/node/.openclaw/operator-tmp \
|
|
--env TMPDIR=/home/node/.openclaw/operator-tmp
|
|
run_case image-user 1000 /home/node/.openclaw/cache '' ''
|
|
run_case root-invoker 0 /home/node/.openclaw/cache '' ''
|
|
|
|
(
|
|
umask 022
|
|
prepare_install_prefix "$scratch/previous-runtime" openclaw@2026.8.2
|
|
timeout --foreground 600s npm install --prefix "$scratch/previous-runtime" --no-save --no-package-lock \
|
|
--no-audit --no-fund openclaw@2026.8.2
|
|
)
|
|
"$node_bin" -e '
|
|
const assert = require("node:assert/strict");
|
|
const pkg = require(process.argv[1]);
|
|
assert.equal(pkg.version, "2026.8.2");
|
|
console.log(JSON.stringify({ previousCliVersion: pkg.version }));
|
|
' "$scratch/previous-runtime/node_modules/openclaw/package.json"
|
|
run_previous_default_case generated upgrade
|
|
run_previous_default_case explicit upgrade
|
|
run_previous_default_case generated restore
|
|
run_previous_default_case explicit restore
|
|
|
|
bash "$ROOT_DIR/scripts/e2e/lib/fleet-cache/podman-control.sh" "$node_bin" "$cli_entry" "$image"
|