20260723 新增 rpki-explorer 单容器部署资产并修复 parsed 投影渲染(#128):deploy/rpki-explorer 全套独立 compose 部署资产(对接 231 在跑 soak、retain 10 runs、SSH 转发访问);ProjectionView 按真实 API 结构解包 projection.object、证书走 resourceCertificate+extensions、CRL 读顶层字段并新增 IP/AS 资源格式化;e2e fixtures 对齐真实嵌套结构并新增证书/CRL parsed 覆盖
This commit is contained in:
22
deploy/rpki-explorer/.env.example
Normal file
22
deploy/rpki-explorer/.env.example
Normal file
@@ -0,0 +1,22 @@
|
||||
# rpki-explorer standalone stack configuration.
|
||||
# Copy to .env and adjust paths for the target host.
|
||||
|
||||
# Soak outputs on the host (mounted read-only).
|
||||
SOAK_RUNS_DIR=/root/rpki096_x86_rtr_installer/data/runs
|
||||
SOAK_DB_DIR=/root/rpki096_x86_rtr_installer/data/state/db
|
||||
|
||||
# Host directory where the query-db index is persisted (reused across restarts).
|
||||
QUERY_DB_DIR=/root/rpki128_explorer/query-db
|
||||
|
||||
# Host loopback port the explorer is published on (SSH port-forward access).
|
||||
LISTEN_PORT=9517
|
||||
|
||||
# Keep only the latest N indexed runs; first boot backfills the latest N runs.
|
||||
RETAIN_RUNS=10
|
||||
|
||||
# Watch poll interval for newly completed runs.
|
||||
WATCH_INTERVAL_SECS=10
|
||||
|
||||
# Image coordinates (built locally, transferred via docker save/load).
|
||||
IMAGE_NAME=rpki-explorer
|
||||
IMAGE_TAG=local
|
||||
68
deploy/rpki-explorer/Dockerfile
Normal file
68
deploy/rpki-explorer/Dockerfile
Normal file
@@ -0,0 +1,68 @@
|
||||
# rpki-explorer all-in-one image:
|
||||
# - rpki_query_service (watch mode) + rpki_query_indexer
|
||||
# - nginx serving the explorer SPA and proxying /api/v1/ to the query service
|
||||
#
|
||||
# Build context must be the repository root (rpki_2/rpki):
|
||||
# docker build -f deploy/rpki-explorer/Dockerfile -t rpki-explorer:<tag> .
|
||||
#
|
||||
# Note: no `# syntax=` directive on purpose — the default images below are all
|
||||
# expected to be present locally, and pulling the dockerfile frontend from
|
||||
# docker.io may not be possible on offline build hosts.
|
||||
|
||||
ARG RUST_BUILDER_IMAGE=rust:1-bookworm
|
||||
ARG NODE_BUILDER_IMAGE=node:slim
|
||||
ARG RUNTIME_IMAGE=nginx:1.27-bookworm
|
||||
|
||||
FROM ${RUST_BUILDER_IMAGE} AS rust-builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
clang \
|
||||
cmake \
|
||||
git \
|
||||
libclang-dev \
|
||||
make \
|
||||
perl \
|
||||
pkg-config \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/usr/local/cargo/git \
|
||||
--mount=type=cache,target=/src/target \
|
||||
cargo build --release --bin rpki_query_service --bin rpki_query_indexer \
|
||||
&& cp target/release/rpki_query_service target/release/rpki_query_indexer /usr/local/bin/
|
||||
|
||||
FROM ${NODE_BUILDER_IMAGE} AS ui-builder
|
||||
|
||||
WORKDIR /ui
|
||||
|
||||
COPY ui/rpki-explorer/package.json ui/rpki-explorer/package-lock.json ./
|
||||
RUN npm ci --no-audit --no-fund
|
||||
|
||||
COPY ui/rpki-explorer/ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM ${RUNTIME_IMAGE}
|
||||
|
||||
ARG GIT_REV=unknown
|
||||
LABEL org.opencontainers.image.title="rpki-explorer" \
|
||||
org.opencontainers.image.revision="${GIT_REV}"
|
||||
|
||||
COPY --from=rust-builder /usr/local/bin/rpki_query_service /usr/local/bin/rpki_query_service
|
||||
COPY --from=rust-builder /usr/local/bin/rpki_query_indexer /usr/local/bin/rpki_query_indexer
|
||||
COPY --from=ui-builder /ui/dist /usr/share/nginx/html
|
||||
COPY deploy/rpki-explorer/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY deploy/rpki-explorer/entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh && mkdir -p /data
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
76
deploy/rpki-explorer/README.md
Normal file
76
deploy/rpki-explorer/README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# rpki-explorer standalone stack
|
||||
|
||||
One container running the RPKI Explorer web UI plus the query backend
|
||||
(`rpki_query_service` in watch mode, spawning `rpki_query_indexer`), deployed
|
||||
next to a running ours-RP soak. It mounts the soak outputs **read-only** and
|
||||
keeps only the latest `RETAIN_RUNS` indexed runs.
|
||||
|
||||
```text
|
||||
browser ──ssh -L──► 127.0.0.1:9517 ──► container
|
||||
├─ nginx :8080 (dist + /api/v1/ proxy)
|
||||
└─ rpki_query_service :9557
|
||||
├─ /data/query-db (host bind mount, rw, reused on restart)
|
||||
├─ /soak/db/repo-bytes.db (ro)
|
||||
└─ /soak/runs (ro, watched)
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker with the compose plugin (`docker compose version`) on the target host.
|
||||
- A running soak whose `runs/` directories contain `report.json` (and
|
||||
`run-summary.json` once finished) — only completed runs get indexed.
|
||||
|
||||
## Build (local machine, same arch as the target)
|
||||
|
||||
```bash
|
||||
deploy/rpki-explorer/build_image.sh
|
||||
# tarball + provenance land in target/rpki-explorer-image/
|
||||
```
|
||||
|
||||
The build runs entirely inside Docker (rust release binaries + vite build),
|
||||
so no local toolchain version requirements apply.
|
||||
|
||||
## Transfer and deploy (remote host)
|
||||
|
||||
```bash
|
||||
cat target/rpki-explorer-image/rpki-explorer-<tag>.tar.gz | ssh root@<host> 'gunzip | docker load'
|
||||
|
||||
ssh root@<host>
|
||||
mkdir -p /root/rpki128_explorer
|
||||
cd /root/rpki128_explorer
|
||||
# place docker-compose.yml and .env here (see .env.example)
|
||||
docker compose up -d
|
||||
docker logs -f rpki-explorer
|
||||
```
|
||||
|
||||
On first boot the entrypoint computes
|
||||
`--watch-min-run-seq = latest_completed - RETAIN_RUNS + 1` so exactly the
|
||||
latest `RETAIN_RUNS` completed runs are backfilled. On later boots the
|
||||
existing query-db drives continuation — the index built earlier is reused, no
|
||||
re-backfill happens.
|
||||
|
||||
## Access
|
||||
|
||||
```bash
|
||||
ssh -L 9517:127.0.0.1:9517 root@<host>
|
||||
# open http://127.0.0.1:9517
|
||||
```
|
||||
|
||||
The stack binds loopback only; there is no authentication or TLS.
|
||||
|
||||
## Upgrade
|
||||
|
||||
Rebuild the image with a new tag, transfer and `docker load`, update
|
||||
`IMAGE_TAG` in `.env`, then `docker compose up -d`. The query-db on the host
|
||||
is preserved.
|
||||
|
||||
## Rollback / removal
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
# keep /root/rpki128_explorer/query-db to reuse the index later, or delete it
|
||||
# to force a fresh backfill of the latest RETAIN_RUNS runs.
|
||||
```
|
||||
|
||||
The soak stack is never touched; removing the explorer stack has no effect on
|
||||
it.
|
||||
37
deploy/rpki-explorer/build_image.sh
Executable file
37
deploy/rpki-explorer/build_image.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the rpki-explorer all-in-one image locally and export it as a tarball
|
||||
# ready to transfer to a remote host (docker load on the other side).
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
OUT_DIR="${OUT_DIR:-$REPO_ROOT/target/rpki-explorer-image}"
|
||||
IMAGE_NAME="${IMAGE_NAME:-rpki-explorer}"
|
||||
GIT_REV="$(git -C "$REPO_ROOT" rev-parse --short HEAD)"
|
||||
IMAGE_TAG="${IMAGE_TAG:-$GIT_REV}"
|
||||
|
||||
dirty_count="$(git -C "$REPO_ROOT" status --porcelain | wc -l)"
|
||||
|
||||
echo "[build] repo=$REPO_ROOT rev=$GIT_REV dirty_files=$dirty_count tag=$IMAGE_TAG"
|
||||
|
||||
docker build \
|
||||
--file "$REPO_ROOT/deploy/rpki-explorer/Dockerfile" \
|
||||
--build-arg "GIT_REV=$GIT_REV" \
|
||||
--tag "$IMAGE_NAME:$IMAGE_TAG" \
|
||||
"$REPO_ROOT"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
TARBALL="$OUT_DIR/rpki-explorer-$IMAGE_TAG.tar.gz"
|
||||
docker save "$IMAGE_NAME:$IMAGE_TAG" | gzip > "$TARBALL"
|
||||
|
||||
IMAGE_ID="$(docker image inspect --format '{{.Id}}' "$IMAGE_NAME:$IMAGE_TAG")"
|
||||
cat > "$OUT_DIR/rpki-explorer-$IMAGE_TAG.provenance.txt" <<EOF
|
||||
git_rev=$GIT_REV
|
||||
dirty_files=$dirty_count
|
||||
build_time_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
image=$IMAGE_NAME:$IMAGE_TAG
|
||||
image_id=$IMAGE_ID
|
||||
EOF
|
||||
|
||||
echo "[build] image tarball: $TARBALL"
|
||||
echo "[build] provenance: $OUT_DIR/rpki-explorer-$IMAGE_TAG.provenance.txt"
|
||||
echo "[build] transfer: cat '$TARBALL' | ssh root@<host> 'gunzip | docker load'"
|
||||
28
deploy/rpki-explorer/docker-compose.yml
Normal file
28
deploy/rpki-explorer/docker-compose.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
name: rpki-explorer
|
||||
|
||||
# Standalone explorer stack: one container with nginx + rpki_query_service
|
||||
# (+ rpki_query_indexer spawned by the service watcher). It mounts the soak
|
||||
# run root and state db read-only and persists its query-db on the host so
|
||||
# container/host restarts reuse the already-built index.
|
||||
|
||||
services:
|
||||
rpki-explorer:
|
||||
image: ${IMAGE_NAME:-rpki-explorer}:${IMAGE_TAG:-local}
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/rpki-explorer/Dockerfile
|
||||
args:
|
||||
GIT_REV: ${GIT_REV:-unknown}
|
||||
container_name: rpki-explorer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
# Loopback only: access via SSH port forwarding, e.g.
|
||||
# ssh -L 9517:127.0.0.1:9517 root@<host>
|
||||
- "127.0.0.1:${LISTEN_PORT:-9517}:8080"
|
||||
environment:
|
||||
RETAIN_RUNS: "${RETAIN_RUNS:-10}"
|
||||
WATCH_INTERVAL_SECS: "${WATCH_INTERVAL_SECS:-10}"
|
||||
volumes:
|
||||
- "${SOAK_RUNS_DIR:?set SOAK_RUNS_DIR in .env}:/soak/runs:ro"
|
||||
- "${SOAK_DB_DIR:?set SOAK_DB_DIR in .env}:/soak/db:ro"
|
||||
- "${QUERY_DB_DIR:-./query-db}:/data"
|
||||
92
deploy/rpki-explorer/entrypoint.sh
Executable file
92
deploy/rpki-explorer/entrypoint.sh
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# Container entrypoint: run rpki_query_service (watch mode) and nginx in one
|
||||
# container. nginx serves the explorer SPA and proxies /api/v1/ to the query
|
||||
# service. If either process exits, the container exits and the compose
|
||||
# restart policy brings it back.
|
||||
set -euo pipefail
|
||||
|
||||
RUNS_DIR="${RUNS_DIR:-/soak/runs}"
|
||||
REPO_BYTES_DB="${REPO_BYTES_DB:-/soak/db/repo-bytes.db}"
|
||||
QUERY_DB="${QUERY_DB:-/data/query-db}"
|
||||
RETAIN_RUNS="${RETAIN_RUNS:-10}"
|
||||
WATCH_INTERVAL_SECS="${WATCH_INTERVAL_SECS:-10}"
|
||||
QUERY_LISTEN="${QUERY_LISTEN:-127.0.0.1:9557}"
|
||||
|
||||
log() { echo "[entrypoint] $*"; }
|
||||
|
||||
latest_completed_seq() {
|
||||
local latest=0 dir base n
|
||||
shopt -s nullglob
|
||||
for dir in "$RUNS_DIR"/run_*/; do
|
||||
[ -f "${dir}report.json" ] || continue
|
||||
base="${dir%/}"
|
||||
n="${base##*/run_}"
|
||||
case "$n" in
|
||||
*[!0-9]* | "") continue ;;
|
||||
esac
|
||||
if [ "$n" -gt "$latest" ]; then
|
||||
latest="$n"
|
||||
fi
|
||||
done
|
||||
echo "$latest"
|
||||
}
|
||||
|
||||
mkdir -p "$(dirname "$QUERY_DB")"
|
||||
|
||||
ARGS=(
|
||||
--query-db "$QUERY_DB"
|
||||
--listen "$QUERY_LISTEN"
|
||||
--watch-run-root "$RUNS_DIR"
|
||||
--watch-interval-secs "$WATCH_INTERVAL_SECS"
|
||||
--retain-indexed-runs "$RETAIN_RUNS"
|
||||
)
|
||||
|
||||
if [ -e "$REPO_BYTES_DB" ]; then
|
||||
ARGS+=(--repo-bytes-db "$REPO_BYTES_DB")
|
||||
else
|
||||
log "WARNING: repo-bytes db not found at $REPO_BYTES_DB; parsed/raw/export features will be unavailable"
|
||||
fi
|
||||
|
||||
# Decide the watcher start point:
|
||||
# - explicit WATCH_MIN_RUN_SEQ always wins;
|
||||
# - on first boot (query-db does not exist yet) backfill the latest
|
||||
# RETAIN_RUNS completed runs;
|
||||
# - on later boots the existing query-db drives continuation
|
||||
# (max(min_run_seq, latest_ready+1)), so no flag is needed.
|
||||
if [ -n "${WATCH_MIN_RUN_SEQ:-}" ]; then
|
||||
ARGS+=(--watch-min-run-seq "$WATCH_MIN_RUN_SEQ")
|
||||
log "using explicit WATCH_MIN_RUN_SEQ=$WATCH_MIN_RUN_SEQ"
|
||||
elif [ ! -e "$QUERY_DB" ]; then
|
||||
latest="$(latest_completed_seq)"
|
||||
if [ "$latest" -gt "$RETAIN_RUNS" ]; then
|
||||
min_seq=$((latest - RETAIN_RUNS + 1))
|
||||
else
|
||||
min_seq=1
|
||||
fi
|
||||
log "first boot: latest completed run seq=$latest -> --watch-min-run-seq $min_seq (backfill $RETAIN_RUNS runs)"
|
||||
ARGS+=(--watch-min-run-seq "$min_seq")
|
||||
else
|
||||
log "existing query-db at $QUERY_DB; resuming from indexed state"
|
||||
fi
|
||||
|
||||
log "starting rpki_query_service ${ARGS[*]}"
|
||||
rpki_query_service "${ARGS[@]}" &
|
||||
QS_PID=$!
|
||||
|
||||
log "starting nginx"
|
||||
nginx -g 'daemon off;' &
|
||||
NGINX_PID=$!
|
||||
|
||||
shutdown() {
|
||||
log "shutting down (qs=$QS_PID nginx=$NGINX_PID)"
|
||||
kill -TERM "$QS_PID" 2>/dev/null || true
|
||||
kill -TERM "$NGINX_PID" 2>/dev/null || true
|
||||
}
|
||||
trap shutdown TERM INT
|
||||
|
||||
wait -n "$QS_PID" "$NGINX_PID"
|
||||
EXIT_CODE=$?
|
||||
log "a process exited (code=$EXIT_CODE); stopping the remaining one"
|
||||
shutdown
|
||||
wait "$QS_PID" "$NGINX_PID" 2>/dev/null || true
|
||||
exit "$EXIT_CODE"
|
||||
27
deploy/rpki-explorer/nginx.conf
Normal file
27
deploy/rpki-explorer/nginx.conf
Normal file
@@ -0,0 +1,27 @@
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
|
||||
access_log /dev/stdout;
|
||||
error_log /dev/stderr;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# query service has no CORS headers and no authentication: the API must be
|
||||
# served same-origin with the UI.
|
||||
location /api/v1/ {
|
||||
proxy_pass http://127.0.0.1:9557;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
|
||||
# SPA fallback: routing is client-side.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user