mirror of
https://github.com/lukaszraczylo/traefik-llmgateway.git
synced 2026-10-12 04:54:45 +00:00
Single-file file-provider watch (providers.file.filename) never fires under Docker Desktop's VirtioFS bind mount — empirically confirmed against this stack. Traefik's directory-based file provider (providers.file.directory) is the fix, and is also the correct pattern for a Kubernetes ConfigMap mount (a symlink swap, which a single-file watch never observes either). integration/traefik/traefik.yml: providers.file.filename -> providers.file.directory (watch: true unchanged). docker-compose.yml: both traefik1 and traefik2 now bind-mount integration/traefik/dynamic/ (a directory) instead of the single rendered dynamic.yml. Makefile's integration-up now mkdir -p's and renders into integration/traefik/dynamic/dynamic.yml; .gitignore updated to match. New TestConfigHotReload (build tag integration): baseline chat succeeds, then a sustained ~20rps request loop runs against an existing model while the rendered config is rewritten (temp file + os.Rename, atomic, mirroring a k8s symlink swap) to add a new model alias. /v1/models is polled up to 30s for the alias to appear. The request loop's zero- failures property is asserted unconditionally, whether or not the watch fired within the bound; only when it fired does the test also chat through the new alias and expect 200. Empirically, on this host the directory watch fires reliably but with variable latency (observed under 1s up to several seconds; one run exceeded the 30s bound and hit the Skip path with zero request failures throughout). Found and fixed during verification: the test's cleanup step now writes the restore atomically and WAITS for it to propagate before returning. Without that wait, a delayed rebuild triggered by the restore could land during a later test's own non-atomic config write (TestUsersFileHotReload's users.json), reading a torn file mid-write and failing plugin construction outright — a global 404 outage until the next unrelated config change happened to trigger a successful rebuild. Reproduced and confirmed fixed: two full `make integration` runs (cold teardown-to-up, then immediately again) both pass end to end.
81 lines
3.1 KiB
Makefile
81 lines
3.1 KiB
Makefile
# Serialize targets even under `make -j`: the integration targets are
|
|
# stateful (one docker compose stack, one set of host ports) and running
|
|
# two of them concurrently would corrupt each other's containers.
|
|
.NOTPARALLEL:
|
|
|
|
.PHONY: test lint yaegi-check integration integration-keep integration-up integration-wait integration-down
|
|
|
|
test:
|
|
go test ./... -count=1
|
|
|
|
lint:
|
|
@fmt_out="$$(gofmt -l .)"; \
|
|
if [ -n "$$fmt_out" ]; then \
|
|
echo "gofmt: files not formatted:"; \
|
|
echo "$$fmt_out"; \
|
|
exit 1; \
|
|
fi
|
|
go vet ./...
|
|
|
|
yaegi-check:
|
|
cd tools/yaegi-check && GOWORK=off go run . $(CURDIR)
|
|
|
|
INTEGRATION_COMPOSE := integration/docker-compose.yml
|
|
INTEGRATION_HEALTH_URL := http://localhost:19081/v1/models
|
|
INTEGRATION_ALICE_KEY := sk-int-alice
|
|
|
|
# Overridable so the optional real-upstream smoke test (task 7,
|
|
# INTEGRATION_REAL=1) never hardcodes one operator's personal hostname as
|
|
# the only option: `INTEGRATION_REAL_BASEURL=https://other.example make
|
|
# integration` points it elsewhere. Rendered into
|
|
# integration/traefik/dynamic/dynamic.yml (git-ignored directory) from the
|
|
# checked-in dynamic.yml.tmpl by integration-up, below. Traefik's file
|
|
# provider watches that whole directory (traefik.yml's providers.file.directory),
|
|
# not the single rendered file — see traefik.yml's own comment for why.
|
|
INTEGRATION_REAL_BASEURL ?= https://llmgw.example.com
|
|
|
|
integration-up:
|
|
mkdir -p integration/traefik/dynamic
|
|
sed 's#__INTEGRATION_REAL_BASEURL__#$(INTEGRATION_REAL_BASEURL)#' integration/traefik/dynamic.yml.tmpl > integration/traefik/dynamic/dynamic.yml
|
|
docker compose -f $(INTEGRATION_COMPOSE) up -d --build
|
|
|
|
integration-wait:
|
|
@i=0; \
|
|
until curl -sf -o /dev/null -H "Authorization: Bearer $(INTEGRATION_ALICE_KEY)" $(INTEGRATION_HEALTH_URL); do \
|
|
i=$$((i+1)); \
|
|
if [ $$i -ge 120 ]; then \
|
|
echo "integration: traefik1 did not become healthy within 120s"; \
|
|
docker compose -f $(INTEGRATION_COMPOSE) logs traefik1; \
|
|
exit 1; \
|
|
fi; \
|
|
sleep 1; \
|
|
done
|
|
|
|
integration-down:
|
|
docker compose -f $(INTEGRATION_COMPOSE) down -v
|
|
|
|
# integration brings the stack up, waits for it to answer, runs the suite,
|
|
# then always tears the stack down — pass, fail, or a failure to even come
|
|
# up or become healthy — so a broken run never leaves a stray stack behind.
|
|
# integration-up/-wait run as recipe-body sub-makes (not prerequisites)
|
|
# specifically so a non-zero status from either one still falls through to
|
|
# integration-down instead of Make aborting before teardown ever runs.
|
|
integration:
|
|
@$(MAKE) integration-up; status=$$?; \
|
|
if [ $$status -eq 0 ]; then \
|
|
$(MAKE) integration-wait; status=$$?; \
|
|
fi; \
|
|
if [ $$status -eq 0 ]; then \
|
|
go -C integration test -tags integration -count=1 ./...; status=$$?; \
|
|
fi; \
|
|
$(MAKE) integration-down; \
|
|
exit $$status
|
|
|
|
# integration-keep is the debugging variant: same run, but leaves the
|
|
# compose stack up afterward for inspecting logs/state by hand (including
|
|
# on a bring-up/health failure, deliberately — there's nothing to inspect
|
|
# post-mortem if it tore itself down). Tear it down yourself with
|
|
# `make integration-down` when done.
|
|
integration-keep: integration-up integration-wait
|
|
go -C integration test -tags integration -count=1 ./...
|