f4bee442db
- Mount agents-sanitized.json (agents key only, no secrets) instead of full openclaw.json - Update AgentService default path from /home/node/.openclaw/openclaw.json to /etc/nexus/agents-sanitized.json - Add AgentConfigPath env var to compose for explicit path configuration - Generate sanitized file in deploy-nexus.sh before each deploy using Python extraction - Add agents-sanitized.json to .gitignore Eliminates the fragile ACL on openclaw.json (uid 1654) that causes 500 errors on the Board endpoint when lost.
265 lines
7.9 KiB
Bash
Executable File
265 lines
7.9 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
DEPLOY_PATH="${DEPLOY_PATH:-/home/projekte_bao/nexus}"
|
|
ENV_TMPFILE_TEMPLATE="${ENV_TMPFILE:-/tmp/nexus-deploy-env}"
|
|
ENV_TMPFILE=""
|
|
BASE_URL="${BASE_URL:-https://nexus.noveria.net}"
|
|
BOOTSTRAP_OWNER_EMAIL="${BOOTSTRAP_OWNER_EMAIL_DEPLOY:-vmbao62@hotmail.de}"
|
|
|
|
cleanup() {
|
|
if [ -n "$ENV_TMPFILE" ] && [ -f "$ENV_TMPFILE" ]; then
|
|
shred -u "$ENV_TMPFILE" 2>/dev/null || rm -f "$ENV_TMPFILE"
|
|
fi
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
require_env() {
|
|
name="$1"
|
|
eval "value=\${$name:-}"
|
|
if [ -z "$value" ]; then
|
|
echo "Missing required environment variable: $name" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_env ENV_POSTGRES_PASSWORD
|
|
require_env ENV_JWT_KEY
|
|
|
|
secure_tmpfile() {
|
|
template="$1"
|
|
dir="$(dirname "$template")"
|
|
base="$(basename "$template")"
|
|
mkdir -p "$dir"
|
|
mktemp "$dir/$base.XXXXXX"
|
|
}
|
|
|
|
ENV_TMPFILE="$(secure_tmpfile "$ENV_TMPFILE_TEMPLATE")"
|
|
chmod 600 "$ENV_TMPFILE"
|
|
|
|
if [ ! -f VERSION ]; then
|
|
echo "VERSION file not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
VERSION="$(tr -d '[:space:]' < VERSION)"
|
|
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
|
echo "Invalid VERSION value: $VERSION" >&2
|
|
exit 1
|
|
fi
|
|
|
|
GIT_SHA="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
GIT_REF="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
|
echo "Deploying Nexus v$VERSION from $GIT_REF"
|
|
|
|
umask 077
|
|
cat > "$ENV_TMPFILE" <<EOF_ENV
|
|
POSTGRES_DB=nexus
|
|
POSTGRES_USER=nexus
|
|
POSTGRES_PASSWORD=${ENV_POSTGRES_PASSWORD}
|
|
JWT_KEY=${ENV_JWT_KEY}
|
|
JWT_ISSUER=nexus
|
|
JWT_AUDIENCE=nexus-web
|
|
BOOTSTRAP_OWNER_EMAIL=${BOOTSTRAP_OWNER_EMAIL}
|
|
OPENCLAW_BASE_URL=http://openclaw-gateway-bao:18789
|
|
OPENCLAW_GATEWAY_TOKEN=${ENV_OPENCLAW_TOKEN:-}
|
|
OPENCLAW_GATEWAY_PASSWORD=
|
|
NEXUS_VERSION=${VERSION}
|
|
NEXUS_GIT_SHA=${GIT_SHA}
|
|
EOF_ENV
|
|
|
|
echo "Syncing source to deploy path: $DEPLOY_PATH"
|
|
git archive --format=tar HEAD | docker run --rm -i \
|
|
-v "$DEPLOY_PATH:/dest" \
|
|
alpine:3.20 \
|
|
sh -c '
|
|
set -eu
|
|
dest_owner="$(stat -c "%u:%g" /dest)"
|
|
mkdir -p /src-snapshot
|
|
tar -xf - -C /src-snapshot
|
|
|
|
is_protected_path() {
|
|
case "$1" in
|
|
./.git|./.git/*|./.env|./.env.*|./data|./data/*|./logs|./logs/*|./backups|./backups/*|./tmp|./tmp/*|./uploads|./uploads/*|./storage|./storage/*)
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cd /dest
|
|
find . -mindepth 1 -maxdepth 1 | while IFS= read -r path; do
|
|
if ! is_protected_path "$path"; then
|
|
rm -rf "$path"
|
|
fi
|
|
done
|
|
|
|
cd /src-snapshot
|
|
find . -mindepth 1 -maxdepth 1 | while IFS= read -r path; do
|
|
if ! is_protected_path "$path"; then
|
|
cp -a "$path" /dest/
|
|
fi
|
|
done
|
|
|
|
chown -R "$dest_owner" /dest
|
|
'
|
|
|
|
# ── Sanitized agents config for Nexus (no secrets) ──
|
|
echo "Generating sanitized agents config for Nexus"
|
|
AGENTS_SANITIZED_PATH="/home/projekte_bao/openclaw/data/openclaw/agents-sanitized.json"
|
|
OPENCLAW_CONFIG="/home/projekte_bao/openclaw/data/openclaw/openclaw.json"
|
|
if [ -f "$OPENCLAW_CONFIG" ]; then
|
|
python3 -c "
|
|
import json, sys
|
|
with open('$OPENCLAW_CONFIG') as f:
|
|
data = json.load(f)
|
|
agents = data.get('agents')
|
|
if agents is None:
|
|
print('ERROR: \"agents\" key not found in openclaw.json', file=sys.stderr)
|
|
sys.exit(1)
|
|
with open('$AGENTS_SANITIZED_PATH', 'w') as f:
|
|
json.dump({'agents': agents}, f, indent=2)
|
|
"
|
|
chmod 644 "$AGENTS_SANITIZED_PATH" 2>/dev/null || true
|
|
echo "Sanitized agents config written to $AGENTS_SANITIZED_PATH"
|
|
else
|
|
echo "WARNING: openclaw.json not found at $OPENCLAW_CONFIG — agents-sanitized.json NOT generated" >&2
|
|
fi
|
|
|
|
echo "Building and starting Docker compose stack"
|
|
docker run --rm \
|
|
-v "$DEPLOY_PATH:/workspace/nexus" \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
-w /workspace/nexus \
|
|
-i \
|
|
docker:cli \
|
|
sh -c 'set -eu
|
|
umask 077
|
|
cat > /tmp/nexus-deploy-env
|
|
trap '\''rm -f /tmp/nexus-deploy-env'\'' EXIT INT TERM
|
|
docker compose --env-file /tmp/nexus-deploy-env build
|
|
|
|
# ── Postgres: only recreate if image or config changed ──
|
|
# docker compose up -d (without --force-recreate) is smart enough
|
|
# to only recreate containers whose config or image has changed.
|
|
# We DROP --force-recreate so postgres persists across deploys
|
|
# unless its image tag or compose config actually changed.
|
|
docker compose --env-file /tmp/nexus-deploy-env up -d --remove-orphans --wait
|
|
|
|
docker compose --env-file /tmp/nexus-deploy-env ps
|
|
' < "$ENV_TMPFILE"
|
|
|
|
echo "Verifying image provenance"
|
|
for container in nexus-api-1 nexus-web-1; do
|
|
revision="$(docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' "$container")"
|
|
version="$(docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' "$container")"
|
|
if [ "$revision" != "$GIT_SHA" ]; then
|
|
echo "Image revision mismatch for $container: expected $GIT_SHA, got $revision" >&2
|
|
exit 1
|
|
fi
|
|
if [ "$version" != "$VERSION" ]; then
|
|
echo "Image version mismatch for $container: expected $VERSION, got $version" >&2
|
|
exit 1
|
|
fi
|
|
echo "$container provenance verified: v$version $revision"
|
|
done
|
|
|
|
echo "Checking live health"
|
|
retry=0
|
|
while [ "$retry" -lt 6 ]; do
|
|
retry=$((retry + 1))
|
|
health_body="$(curl -fsS --max-time 10 "$BASE_URL/health" 2>/dev/null || true)"
|
|
case "$health_body" in
|
|
'{"status":"Healthy"'*)
|
|
echo "Health check passed"
|
|
break
|
|
;;
|
|
esac
|
|
if [ -n "$health_body" ]; then
|
|
echo "Health endpoint is reachable but not healthy: $health_body" >&2
|
|
fi
|
|
if [ "$retry" -eq 6 ]; then
|
|
echo "Health check failed" >&2
|
|
exit 1
|
|
fi
|
|
sleep "$retry"
|
|
done
|
|
|
|
pass=0
|
|
fail=0
|
|
check() {
|
|
path="$1"
|
|
expected="$2"
|
|
label="$3"
|
|
code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 "$BASE_URL$path")"
|
|
printf '%-28s HTTP %s\n' "$label" "$code"
|
|
if [ "$code" = "$expected" ]; then
|
|
pass=$((pass + 1))
|
|
else
|
|
fail=$((fail + 1))
|
|
fi
|
|
}
|
|
|
|
check_post() {
|
|
path="$1"
|
|
expected="$2"
|
|
label="$3"
|
|
code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 10 -X POST -H 'Content-Type: application/json' --data '{}' "$BASE_URL$path")"
|
|
printf '%-28s HTTP %s\n' "$label" "$code"
|
|
if [ "$code" = "$expected" ]; then
|
|
pass=$((pass + 1))
|
|
else
|
|
fail=$((fail + 1))
|
|
fi
|
|
}
|
|
|
|
check "/dashboard" "200" "Dashboard"
|
|
check "/health" "200" "Health"
|
|
check "/api/v1/operations/snapshot" "401" "Operations auth"
|
|
check_post "/api/v1/chat" "401" "Chat auth"
|
|
|
|
# ── Auth Smoke: SeedAudit owner_created exists in DB ──
|
|
echo ""
|
|
echo "Auth Smoke: SeedAudit owner_created"
|
|
seed_key="$(docker exec nexus-postgres-1 psql -U nexus -d nexus -t -A -c "SELECT key FROM \"SeedAudit\" WHERE key = 'owner_created'" 2>/dev/null || echo "")"
|
|
seed_key="$(echo "$seed_key" | tr -d '[:space:]')"
|
|
if [ "$seed_key" = "owner_created" ]; then
|
|
echo " SeedAudit owner_created: ✅ exists"
|
|
pass=$((pass + 1))
|
|
else
|
|
echo " SeedAudit owner_created: ❌ NOT FOUND (DB may not be seeded)" >&2
|
|
echo " Raw output: '$seed_key'" >&2
|
|
fail=$((fail + 1))
|
|
fi
|
|
|
|
# ── Auth Smoke: Owner login flow returns 401 for unknown password ──
|
|
# This proves the user exists, auth pipeline is functional, and the DB is reachable.
|
|
# We POST with a WRONG password intentionally — a 401 means "user found, password wrong",
|
|
# which is the correct auth flow behavior. A 5xx or connection error means the stack is broken.
|
|
echo "Auth Smoke: Owner login flow"
|
|
login_body="$(curl -sS --max-time 10 \
|
|
-X POST \
|
|
-H 'Content-Type: application/json' \
|
|
-d "{\"email\":\"${BOOTSTRAP_OWNER_EMAIL}\",\"password\":\"smoke-test-wrong-password-$(date +%s)\"}" \
|
|
"$BASE_URL/api/v1/auth/login" 2>/dev/null || echo "CONNECTION_ERROR")"
|
|
|
|
if echo "$login_body" | grep -q '"error":"invalid_credentials"'; then
|
|
echo " Owner login flow: ✅ HTTP 401 with valid JSON (auth pipeline working)"
|
|
pass=$((pass + 1))
|
|
else
|
|
echo " Owner login flow: ❌ unexpected response" >&2
|
|
echo " Response: $(echo "$login_body" | head -c 200)" >&2
|
|
fail=$((fail + 1))
|
|
fi
|
|
|
|
if [ "$fail" -ne 0 ]; then
|
|
echo ""
|
|
echo "Smoke test failed: $fail failed, $pass passed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo "Nexus v$VERSION deployed and verified"
|