fix(web): refresh API proxy DNS after backend redeploy (#900)
Some checks are pending
Security / Dependency Review (push) Waiting to run
Security / CodeQL (java-kotlin) (push) Waiting to run
Security / CodeQL (javascript-typescript) (push) Waiting to run
Security / CodeQL (python) (push) Waiting to run

* fix(web): refresh API proxy DNS after backend redeploy

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* test(web): stabilize DNS replacement coverage

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* test(web): allow early resolver refresh after DNS change

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

* test(web): use dynamic ports for nginx smoke

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

---------

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-23 15:51:08 +08:00 • committed by GitHub
parent ed2ff97d00
commit e8fad5962e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 128 additions and 19 deletions

View file

@ -70,6 +70,7 @@ services:
- ./web/dist:/usr/share/nginx/html:ro
- ./web/nginx.conf.template:/etc/nginx/templates/default.conf.template:ro
environment:
NGINX_ENTRYPOINT_LOCAL_RESOLVERS: "1"
SKILLHUB_API_UPSTREAM: http://server:8080
SKILLHUB_WEB_API_BASE_URL: ""
SKILLHUB_PUBLIC_BASE_URL: ""

View file

@ -39,13 +39,25 @@ wait_for_nginx() {
fail "$container did not become healthy"
}
create_test_network() {
local subnet
for subnet in 172.29.0.0/24 172.30.0.0/24 192.168.252.0/24 10.254.0.0/24; do
if docker network create --driver bridge --subnet "$subnet" "$NETWORK" >/dev/null 2>&1; then
return 0
fi
done
fail "could not create a test network with an explicit subnet"
}
start_proxy() {
local container="$1"
local trust_forwarded_proto="$2"
local backend_name="${3:-$BACKEND}"
docker run --detach \
--name "$container" \
--network "$NETWORK" \
--env "SKILLHUB_API_UPSTREAM=http://$BACKEND:8080" \
--env "NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1" \
--env "SKILLHUB_API_UPSTREAM=http://$backend_name:8080" \
--env "SKILLHUB_TRUST_FORWARDED_PROTO=$trust_forwarded_proto" \
--volume "$TEMPLATE:/etc/nginx/templates/default.conf.template:ro" \
"$NGINX_IMAGE" >/dev/null
@ -80,7 +92,7 @@ server {
}
EOF
docker network create "$NETWORK" >/dev/null
create_test_network
docker run --detach \
--name "$BACKEND" \
--network "$NETWORK" \
@ -98,4 +110,89 @@ done
assert_proto "$TRUSTED_PROXY" http
assert_proto "$TRUSTED_PROXY" http "https,http"
echo "nginx-forwarded-proto-test passed"
DNS_BACKEND="${TEST_ID}-dns-backend"
DNS_OLD_BACKEND="${TEST_ID}-dns-old"
DNS_NEW_BACKEND="${TEST_ID}-dns-new"
DNS_STALE_BACKEND="${TEST_ID}-dns-stale"
DNS_PROXY="${TEST_ID}-dns-proxy"
start_dns_backend() {
local container="$1"
local response="$2"
local alias="${3:-}"
local static_ip="${4:-}"
local config="$TMP_DIR/$container.conf"
cat >"$config" <<EOF
server {
listen 8080;
location / {
default_type text/plain;
return 200 "$response";
}
}
EOF
local alias_args=()
local ip_args=()
if [[ -n "$alias" ]]; then
alias_args+=(--network-alias "$alias")
fi
if [[ -n "$static_ip" ]]; then
ip_args+=(--ip "$static_ip")
fi
docker run --detach \
--name "$container" \
--network "$NETWORK" \
"${alias_args[@]}" \
"${ip_args[@]}" \
--volume "$config:/etc/nginx/conf.d/default.conf:ro" \
"$NGINX_IMAGE" >/dev/null
CONTAINERS+=("$container")
}
container_ip() {
docker inspect --format "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" "$1"
}
start_dns_backend "$DNS_OLD_BACKEND" first "$DNS_BACKEND"
start_proxy "$DNS_PROXY" false "$DNS_BACKEND"
assert_dns_response() {
local expected="$1"
local actual
actual="$(docker exec "$DNS_PROXY" wget -qO- http://127.0.0.1/api/dns)" \
|| fail "DNS proxy request failed; expected '$expected'"
[[ "$actual" == "$expected" ]] \
|| fail "DNS proxy returned '$actual', expected '$expected'"
}
assert_dns_response first
old_ip="$(container_ip "$DNS_OLD_BACKEND")"
docker rm -f "$DNS_OLD_BACKEND" >/dev/null
start_dns_backend "$DNS_STALE_BACKEND" stale "" "$old_ip"
start_dns_backend "$DNS_NEW_BACKEND" second "$DNS_BACKEND"
new_ip="$(container_ip "$DNS_NEW_BACKEND")"
[[ "$old_ip" != "$new_ip" ]] || fail "replacement backend reused old IP $old_ip"
refreshed=false
saw_stale_response=false
refresh_started=$SECONDS
for attempt in {1..36}; do
if actual="$(docker exec "$DNS_PROXY" wget -qO- http://127.0.0.1/api/dns 2>/dev/null)"; then
if [[ "$actual" == second ]]; then
refreshed=true
break
fi
[[ "$actual" == stale ]] \
|| fail "DNS proxy returned unexpected backend marker '$actual'"
saw_stale_response=true
fi
sleep 0.5
done
[[ "$refreshed" == true ]] \
|| { docker logs "$DNS_PROXY" >&2 || true; fail "unchanged proxy did not reach replacement backend $new_ip"; }
refresh_elapsed=$((SECONDS - refresh_started))
((refresh_elapsed <= 12)) \
|| fail "DNS refresh took ${refresh_elapsed}s, exceeding the 12s test boundary"
echo "nginx-forwarded-proto-test passed, including DNS refresh ($old_ip -> $new_ip) in ${refresh_elapsed}s; stale cache observed=$saw_stale_response"

View file

@ -15,7 +15,6 @@ fi
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
NGINX_IMAGE="${NGINX_SMOKE_IMAGE:-nginx:alpine}"
name="skillhub-base-path-smoke-$$"
port=18080
tmp=$(mktemp -d)
cleanup() {
@ -24,6 +23,10 @@ cleanup() {
}
trap cleanup EXIT
published_port() {
docker inspect --format '{{(index (index .NetworkSettings.Ports "80/tcp") 0).HostPort}}' "$1"
}
html="$tmp/html"
mkdir -p "$html/assets" "$html/registry"
printf '%s\n' 'INDEX_HTML_MARKER' >"$html/index.html"
@ -40,7 +43,8 @@ cp "$ROOT_DIR/web/docker-entrypoint.d/30-runtime-config.sh" "$entrypoint_d/30-ru
chmod +x "$entrypoint_d/20-base-path.sh" "$entrypoint_d/30-runtime-config.sh"
if ! docker run -d --name "$name" \
-p "$port:80" \
-p 127.0.0.1::80 \
-e NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1 \
-e SKILLHUB_API_UPSTREAM=http://127.0.0.1:9 \
-e SKILLHUB_TRUST_FORWARDED_PROTO=false \
-e SKILLHUB_WEB_BASE_PATH=/skillhub/ \
@ -49,11 +53,12 @@ if ! docker run -d --name "$name" \
-v "$ROOT_DIR/web/nginx.conf.template:/etc/nginx/templates/default.conf.template:ro" \
-v "$entrypoint_d/20-base-path.sh:/docker-entrypoint.d/20-base-path.sh:ro" \
-v "$entrypoint_d/30-runtime-config.sh:/docker-entrypoint.d/30-runtime-config.sh:ro" \
"$NGINX_IMAGE" >/dev/null 2>&1; then
printf '%s\n' 'web-base-path-nginx-smoke-test skipped (docker run failed, e.g. no image/network)'
exit 0
"$NGINX_IMAGE" >/dev/null; then
echo 'nginx container failed to start' >&2
exit 1
fi
port=$(published_port "$name")
base="http://127.0.0.1:$port"
ready=0
i=0
@ -151,9 +156,9 @@ printf '%s\n' 'INDEX_HTML_MARKER' >"$default_html/index.html"
cp "$ROOT_DIR/web/src/docs/skill.md.template" "$default_html/registry/skill.md.template"
cp "$ROOT_DIR/web/runtime-config.js.template" "$default_html/runtime-config.js.template"
name_default="$name-default"
port_default=18082
docker run -d --name "$name_default" \
-p "$port_default:80" \
-p 127.0.0.1::80 \
-e NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1 \
-e SKILLHUB_API_UPSTREAM=http://127.0.0.1:9 \
-e SKILLHUB_TRUST_FORWARDED_PROTO=false \
-e SKILLHUB_WEB_BASE_PATH=/skillhub/ \
@ -163,6 +168,7 @@ docker run -d --name "$name_default" \
-v "$entrypoint_d/30-runtime-config.sh:/docker-entrypoint.d/30-runtime-config.sh:ro" \
"$NGINX_IMAGE" >/dev/null
port_default=$(published_port "$name_default")
default_base="http://127.0.0.1:$port_default"
i=0
until curl -fsS -o /dev/null "$default_base/nginx-health" 2>/dev/null; do
@ -203,9 +209,9 @@ printf '%s\n' 'INDEX_HTML_MARKER' >"$trusted_html/index.html"
cp "$ROOT_DIR/web/src/docs/skill.md.template" "$trusted_html/registry/skill.md.template"
cp "$ROOT_DIR/web/runtime-config.js.template" "$trusted_html/runtime-config.js.template"
name_trusted="$name-trusted"
port_trusted=18083
docker run -d --name "$name_trusted" \
-p "$port_trusted:80" \
-p 127.0.0.1::80 \
-e NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1 \
-e SKILLHUB_API_UPSTREAM=http://127.0.0.1:9 \
-e SKILLHUB_TRUST_FORWARDED_PROTO=true \
-e SKILLHUB_WEB_BASE_PATH=/skillhub/ \
@ -214,6 +220,7 @@ docker run -d --name "$name_trusted" \
-v "$entrypoint_d/20-base-path.sh:/docker-entrypoint.d/20-base-path.sh:ro" \
-v "$entrypoint_d/30-runtime-config.sh:/docker-entrypoint.d/30-runtime-config.sh:ro" \
"$NGINX_IMAGE" >/dev/null
port_trusted=$(published_port "$name_trusted")
trusted_base="http://127.0.0.1:$port_trusted"
i=0
until curl -fsS -o /dev/null "$trusted_base/nginx-health" 2>/dev/null; do
@ -242,10 +249,10 @@ printf '%s\n' 'FIXED_APP_JS_MARKER' >"$fixed_html/assets/app.js"
baked_file="$tmp/baked-base-path"
printf '%s' '/fixed/' >"$baked_file"
fixed_name="$name-fixed"
fixed_port=18081
docker run -d --name "$fixed_name" \
-p "$fixed_port:80" \
-p 127.0.0.1::80 \
-e NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1 \
-e SKILLHUB_API_UPSTREAM=http://127.0.0.1:9 \
-e SKILLHUB_TRUST_FORWARDED_PROTO=false \
-e SKILLHUB_WEB_BASE_PATH= \
@ -254,8 +261,9 @@ docker run -d --name "$fixed_name" \
-v "$baked_file:/etc/skillhub/baked-base-path:ro" \
-v "$ROOT_DIR/web/nginx.conf.template:/etc/nginx/templates/default.conf.template:ro" \
-v "$entrypoint_d/20-base-path.sh:/docker-entrypoint.d/20-base-path.sh:ro" \
"$NGINX_IMAGE" >/dev/null 2>&1
"$NGINX_IMAGE" >/dev/null
fixed_port=$(published_port "$fixed_name")
fixed_base="http://127.0.0.1:$fixed_port"
ready=0
i=0

View file

@ -13,6 +13,7 @@ RUN pnpm build
FROM nginx:alpine
ENV SKILLHUB_TRUST_FORWARDED_PROTO=false
ENV NGINX_ENTRYPOINT_LOCAL_RESOLVERS=1
# Record a fixed build-time base so the entrypoint defaults SKILLHUB_WEB_BASE_PATH
# to it and generates matching Nginx routing without repeating the value at runtime.
# Placeholder builds (the default) intentionally write no file and default to '/'.

View file

@ -2,6 +2,8 @@ server_tokens off;
server {
listen 80;
server_name _;
resolver ${NGINX_LOCAL_RESOLVERS} valid=10s;
set $skillhub_api_upstream "${SKILLHUB_API_UPSTREAM}";
root /usr/share/nginx/html;
index index.html;
@ -34,7 +36,7 @@ server {
}
location /api/ {
proxy_pass ${SKILLHUB_API_UPSTREAM};
proxy_pass $skillhub_api_upstream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -43,7 +45,7 @@ server {
}
location /oauth2/ {
proxy_pass ${SKILLHUB_API_UPSTREAM};
proxy_pass $skillhub_api_upstream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -52,7 +54,7 @@ server {
}
location /login/oauth2/ {
proxy_pass ${SKILLHUB_API_UPSTREAM};
proxy_pass $skillhub_api_upstream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -61,7 +63,7 @@ server {
}
location /.well-known/ {
proxy_pass ${SKILLHUB_API_UPSTREAM};
proxy_pass $skillhub_api_upstream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;