74 lines
3.0 KiB
Bash
74 lines
3.0 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
umask 077
|
|
|
|
base=/opt/endoleap-infra
|
|
foundry=$base/foundry
|
|
origin=https://api.foundry.entroia.com
|
|
candidate=foundry-core-candidate
|
|
network=endoleap-dev_default
|
|
|
|
for name in GOOGLE_CLIENT_ID GOOGLE_CLIENT_SECRET FOUNDRY_PROJECTS; do
|
|
value=${!name-}
|
|
if [[ -z "$value" || "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then
|
|
printf 'Missing or invalid %s\n' "$name" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
test -s "$foundry/database.env"
|
|
docker network inspect "$network" >/dev/null
|
|
|
|
release=$(mktemp -d "$foundry/release.XXXXXXXX")
|
|
install -m 755 "$1" "$release/foundry"
|
|
export HOST=0.0.0.0 PORT=8080 PUBLIC_ORIGIN="$origin"
|
|
docker run -d --name "$candidate" --network "$network" \
|
|
--env-file "$foundry/database.env" -e HOST -e PORT -e PUBLIC_ORIGIN \
|
|
-e GOOGLE_CLIENT_ID -e GOOGLE_CLIENT_SECRET -e FOUNDRY_PROJECTS \
|
|
--user 1000:1000 --cap-drop ALL --security-opt no-new-privileges \
|
|
--read-only --memory 128m \
|
|
--mount "type=bind,source=$release/foundry,target=/opt/foundry,readonly" \
|
|
node:24-bookworm-slim /opt/foundry >/dev/null
|
|
|
|
cleanup_candidate() {
|
|
docker rm -f "${live:-$candidate}" >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup_candidate EXIT
|
|
|
|
docker exec "$candidate" node -e '
|
|
const assert = require("node:assert/strict");
|
|
async function verify() {
|
|
const base = "http://127.0.0.1:8080";
|
|
for (let attempt = 0; ; attempt++) {
|
|
try {
|
|
const ready = await fetch(base + "/readyz", {signal: AbortSignal.timeout(3000)});
|
|
if (ready.ok) break;
|
|
} catch (error) {
|
|
if (attempt === 9) throw error;
|
|
}
|
|
assert.ok(attempt < 9);
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
}
|
|
const project = Object.keys(JSON.parse(process.env.FOUNDRY_PROJECTS))[0];
|
|
const route = base + "/projects/" + encodeURIComponent(project) + "/auth";
|
|
assert.equal((await fetch(route + "/me")).status, 401);
|
|
const challenge = require("node:crypto").randomBytes(32).toString("base64url");
|
|
const start = await fetch(route + "/google?challenge=" + challenge, {redirect: "manual"});
|
|
assert.ok([302, 303].includes(start.status));
|
|
const location = new URL(start.headers.get("location"));
|
|
assert.equal(location.hostname, "accounts.google.com");
|
|
assert.equal(location.searchParams.get("client_id"), process.env.GOOGLE_CLIENT_ID);
|
|
assert.equal(location.searchParams.get("redirect_uri"), process.env.PUBLIC_ORIGIN + "/auth/google/callback");
|
|
}
|
|
verify().catch(() => { console.error("Foundry readiness or OAuth entry check failed"); process.exit(1); });
|
|
'
|
|
|
|
# Keep the previous running container until the new candidate has passed checks.
|
|
# Traefik reads the route atomically from its watched config directory.
|
|
id=$(docker inspect -f '{{.Id}}' "$candidate")
|
|
live=foundry-core-${id:0:12}
|
|
docker rename "$candidate" "$live"
|
|
docker update --restart unless-stopped "$live" >/dev/null
|
|
bash "$(dirname "$0")/route.sh" "$live"
|
|
trap - EXIT
|
|
printf 'Foundry readiness, unauthenticated rejection and Google redirect verified.\n'
|