Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deploy/java/PINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The Reactome Java this deployment builds (spec 010), each pinned to the commit t

| Artifact | Repository | Commit | Runtime |
| -------------------- | --------------------- | ------------------ | ------- |
| pdf-export (`serve`) | `reactome/pdf-export` | `979b8d6` (`main`) | Node 24 |
| pdf-export (`serve`) | `reactome/pdf-export` | `55cde47` (`main`) | Node 24 |

## Changing a pin

Expand Down
6 changes: 6 additions & 0 deletions deploy/nginx/common/routes-core.conf
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,12 @@ location /AnalysisService/ {
# No URI part, as for /ContentService/: the path as sent, never nginx's
# decoded copy of it.
proxy_pass http://analysis;
# Who the client is, for pdf-export's share of the waiting reports
# (pdf-export spec 002, T021): always the address nginx saw, whatever a
# request sent, as pdf-export trusts it. The analysis gateway sets it
# again from its own reading (an IPv6 client by its /64); this keeps a
# value a client chose from passing through where it does not.
proxy_set_header X-Reactome-Client $remote_addr;
# An analysis over a large identifier list is not quick either.
proxy_read_timeout 300s;
proxy_send_timeout 300s;
Expand Down
50 changes: 49 additions & 1 deletion deploy/nginx/test-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,51 @@ cd "$(dirname "$0")/../.."

work=$(mktemp -d)
images=()
containers=()
# The images too: on a shared host they are a few hundred megabytes each.
trap 'rm -rf "$work"; [ ${#images[@]} -eq 0 ] || docker rmi -f "${images[@]}" >/dev/null 2>&1' EXIT
trap '[ ${#containers[@]} -eq 0 ] || docker rm -f "${containers[@]}" >/dev/null 2>&1; rm -rf "$work"; [ ${#images[@]} -eq 0 ] || docker rmi -f "${images[@]}" >/dev/null 2>&1' EXIT
openssl req -x509 -nodes -newkey rsa:2048 -days 1 -subj /CN=localhost \
-keyout "$work/key.pem" -out "$work/cert.pem" 2>/dev/null

# The client the analysis report is counted by (pdf-export spec 002, T021):
# pdf-export trusts X-Reactome-Client for its share of the waiting reports, so
# nginx must set it from the address it saw and never pass on one a request
# chose. Run for real: the deployment's image, its HTTPS server, and a stand-in
# for its `analysis` upstream that answers with the headers it was sent. Both
# share a network namespace with no network, so nothing on this host or
# beyond is reached. A request naming another client in every header a
# client could use is still counted by its own address (here, loopback).
client_named() {
local env=$1 image=$2
shift 2
local conf="deploy/nginx/$env.conf" host port stub server answer
# The server that serves the site's routes, and the analysis upstream's port.
host=$(awk '/server_name/ { n = $2 } /common\/routes\.conf/ { sub(/;$/, "", n); print n; exit }' "$conf")
port=$(awk '/^upstream analysis \{/ { a = 1 } a && /^[[:space:]]*server[[:space:]]/ { sub(/;.*/, ""); n = split($2, p, ":"); print p[n]; exit }' "$conf")
if [ -z "$host" ] || [ -z "$port" ]; then
echo "no site server or analysis upstream found in $conf"
return 1
fi
printf 'server {\n listen 127.0.0.1:%s;\n location / {\n default_type text/plain;\n return 200 "client=$http_x_reactome_client real=$http_x_real_ip";\n }\n}\n' \
"$port" >"$work/stub-$env.conf"
stub=$(docker run -d --network none -v "$work/stub-$env.conf:/etc/nginx/conf.d/default.conf:ro" nginx:alpine)
containers+=("$stub")
server=$(docker run -d --network "container:$stub" "$@" "$image")
containers+=("$server")
for _ in $(seq 50); do
answer=$(docker exec "$stub" curl -sk -m 5 --resolve "$host:443:127.0.0.1" \
-H 'X-Reactome-Client: 203.0.113.9' -H 'X-Real-IP: 203.0.113.9' \
-H 'X-Forwarded-For: 203.0.113.9' -H 'CF-Connecting-IP: 203.0.113.9' \
"https://$host/AnalysisService/report/T/9606/report.pdf" 2>/dev/null) && break
sleep 0.2
done
if [ "$answer" != 'client=127.0.0.1 real=127.0.0.1' ]; then
echo "the analysis service was sent: ${answer:-nothing}"
docker logs "$server" 2>&1 | tail -5
return 1
fi
}

status=0
for env in dev release production; do
# Every certificate and key the configuration names, made under $work.
Expand Down Expand Up @@ -46,6 +86,14 @@ for env in dev release production; do
echo "FAIL $env"
sed 's/^/ /' "$work/out"
status=1
continue
fi
if client_named "$env" "$image" "${mounts[@]}" >"$work/out" 2>&1; then
echo "ok $env: the analysis service is told the client's own address"
else
echo "FAIL $env: a client chose the address it is counted by"
sed 's/^/ /' "$work/out"
status=1
fi
done
exit $status
5 changes: 4 additions & 1 deletion deploy/pdf-export/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
# for an event's PDF at /ContentService/exporter/document/event/{id}.pdf. It
# reads the graph, draws its figures through the render service, reads an
# analysis token's results from the AnalysisService, and prints with Chromium.
# It also answers the analysis report, /AnalysisService/report/... (pdf-export
# spec 002), which the analysis gateway sends it; its printer's working folder
# is compose's tmpfs (PDF_EXPORT_WORK).
#
# Built from the commit deploy/java/PINS.md records ("The PDF service"), from
# GitHub's archive of it: nothing but that commit. pins.spec.ts checks the two
Expand All @@ -12,7 +15,7 @@
# browser, and node:24's layers are already on the host.
FROM node:24

ARG PDF_EXPORT_COMMIT=979b8d6d436c1edcf7e54e104c04c617f12692a8
ARG PDF_EXPORT_COMMIT=55cde47f05832123a685b8c293bb3665465cb32f

# Where the browser goes: outside any user's home, as the service runs as the
# owner of the graph's credentials file, not as the user that installed it
Expand Down
29 changes: 29 additions & 0 deletions deploy/pdf-export/compose.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,35 @@ describe('the PDF service in compose', () => {
expect(dockerfile).toMatch(/"--files", "\/files"/);
});

it('keeps the printer’s working folder in memory, its user’s alone (pdf-export spec 002, T018)', () => {
// A report's page holds what a user submitted, and one killed mid-print
// stays until the service starts again: never on disk.
expect(pdf).toMatch(/^ {6}- PDF_EXPORT_WORK=\/work$/m);
const mount = pdf.match(/^ {4}tmpfs:\n {6}- \/work:(\S+)$/m)?.[1] ?? 'no tmpfs at /work';
const options = mount.split(',');
expect(options).toEqual(
expect.arrayContaining([
'mode=0700',
'uid=${CONTENT_NODE_UID:-1020}',
'gid=${CONTENT_NODE_GID:-1001}',
'noexec',
])
);
expect(options.some((o) => /^size=\d+m$/.test(o))).toBe(true);
});

it('reads the analysis service directly, not through the gateway that sends it reports', () => {
expect(pdf).toMatch(/^ {6}- ANALYSIS_SERVICE=http:\/\/127\.0\.0\.1:8080\/AnalysisService$/m);
});

it('is where the analysis gateway sends the reports (pdf-export spec 002, T021)', () => {
const gateway = service('analysis-gateway');
expect(gateway).toMatch(
/^ {6}- REPORT_UPSTREAM=\$\{REPORT_UPSTREAM:-http:\/\/127\.0\.0\.1:4370\}$/m
);
expect(gateway).not.toMatch(/REPORT_LIMIT/);
});

it('gives Chromium room, and the whole a limit', () => {
expect(pdf).toMatch(/^ {4}shm_size: '1gb'$/m);
expect(pdf).toMatch(/^ {4}mem_limit: \d+[mg]$/m);
Expand Down
30 changes: 24 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ services:
# through unchanged. Host networking, as content-node: Tomcat's
# AnalysisService is on the host's loopback, and nginx reaches this on
# 127.0.0.1:4350. Stateless, so a restart costs only the requests in flight.
#
# The analysis report PDF goes to pdf-export instead (pdf-export spec 002,
# T021), unqueued, with the client named in X-Reactome-Client.
analysis-gateway:
build:
context: .
Expand All @@ -255,8 +258,8 @@ services:
network_mode: host
environment:
- ANALYSIS_UPSTREAM=${ANALYSIS_UPSTREAM:-http://127.0.0.1:8080}
- REPORT_UPSTREAM=${REPORT_UPSTREAM:-http://127.0.0.1:4370}
- ANALYSIS_LIMIT=${ANALYSIS_LIMIT:-4}
- REPORT_LIMIT=${REPORT_LIMIT:-1}
- QUEUE_LENGTH=${QUEUE_LENGTH:-40}
- PER_CLIENT=${PER_CLIENT:-8}
- MAX_WAIT_MS=${MAX_WAIT_MS:-60000}
Expand Down Expand Up @@ -380,8 +383,10 @@ services:
# pdf-export's single-event service (pdf-export spec 001, Story 2) from the
# graph, its figures drawn by the render service as the website draws them,
# an analysis token's results read from the AnalysisService. It replaces
# Java's event-pdf for the website. Node is its only client; nginx never
# names it.
# Java's event-pdf for the website, and, through the analysis gateway, the
# AnalysisService's report PDF (pdf-export spec 002). Node and the gateway
# are its only clients; nginx never names it, as it trusts the client the
# gateway names in X-Reactome-Client.
#
# Built here from the commit deploy/java/PINS.md pins, from GitHub's archive
# of it, and never pulled from or pushed to a registry: no `image:`.
Expand Down Expand Up @@ -415,15 +420,28 @@ services:
# Every figure drawn on request by the render service: no figures'
# folder (pdf-export's `figures`, contracts/figures.md) is given yet.
- RENDER_SERVICE=http://127.0.0.1:4310
# Read for a PDF with an analysis token: the token's results, never a
# new analysis, so not through the analysis gateway's queue.
# Read for a PDF with an analysis token, and for the analysis report
# (pdf-export spec 002, contracts/analysis-api.md): the token's results,
# never a new analysis, so read from the AnalysisService directly, not
# through the analysis gateway, which sends the report here.
- ANALYSIS_SERVICE=http://127.0.0.1:8080/AnalysisService
# The printer's working folder (pdf-export spec 002, FR-211, research
# R7 and R10): a report's page holds the identifiers a user submitted,
# and one killed while it printed leaves its page behind until the
# service starts again. So it is the tmpfs below, in memory and never
# on disk, owned by the service's user alone; the service empties it
# as it starts.
- PDF_EXPORT_WORK=/work
# Where the PDF's links go: the public site, as event-pdf's do.
- REACTOME_SITE=${PDF_SITE:-https://reactome.org}
tmpfs:
- /work:size=256m,mode=0700,uid=${CONTENT_NODE_UID:-1020},gid=${CONTENT_NODE_GID:-1001},noexec,nosuid,nodev
# Chromium prints the PDFs; its default 64 MB /dev/shm is too small for a
# large page, as for the render service.
shm_size: '1gb'
mem_limit: 2g
# Within it: the reports kept for an hour (512 MB at most, pdf-export's
# ServeLimits), two printing at once, and the working folder's tmpfs.
mem_limit: 3g
healthcheck:
test: ['CMD', 'curl', '-fs', '-m', '10', '-o', '/dev/null', 'http://127.0.0.1:4370/health']
interval: 30s
Expand Down
Loading
Loading