Files
traefik-llmgateway/Makefile
T
lukaszraczylo 7b28f34bb2 feat(admin): serve baked vue panel with usage charts
admin.go: GET /admin now serves the generated adminIndexHTML
(admin_assets_gen.go, webui/generate.mjs's output) instead of the old
adminPageHTML Go-string const (removed, ~340 lines, plus its now-dead
io import). New route GET /admin/assets/{hashedname}
(adminAssetsPathPrefix) serves the built app's content-hashed JS/CSS
from the generated adminAssets map - unauthenticated (same rationale as
GET /admin itself), Content-Type per asset, Cache-Control: public,
max-age=31536000, immutable, X-Content-Type-Options: nosniff; an
unknown name is a 404 in the standard OAI error envelope.

CSP tightens: script-src/style-src drop 'unsafe-inline' for 'self' (the
built app has no inline script or style of any kind - vite.config.ts's
build.modulePreload.polyfill:false sees to that), img-src gains 'self'
data:' for the shell's inlined-SVG favicon.

Makefile: new admin-ui target (npm ci && vite build, then
generate.mjs, then gofmt -w) - the only place Node is invoked; CI and
every other make target still need nothing beyond Go.

admin_test.go: TestAdminPage_CSPHeaderAndFetchURLs rewritten as a shell
test against the generated file (new CSP string, body references
/admin/assets/ and the <title>'s "LLM Gateway", no external
src=/href= reference - the favicon's xmlns attribute is excluded from
that check, it names an XML namespace URI, not a fetched resource);
TestAdmin_JSONSecurityHeaders' expected CSP updated; new
TestAdminAsset_Serving (Content-Type/nosniff/immutable-cache/byte-exact
body), TestAdminAsset_UnknownReturns404, and
TestAdminAssets_GeneratedFilePresent (non-empty index.html/assets, no
leftover {{/}}/generator-marker text leaked into the decoded HTML -
proves the base64 round-trip actually worked).
2026-08-21 09:23:37 +01:00

93 lines
3.7 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 admin-ui 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 ./...
# admin-ui rebuilds the Vue admin panel (webui/) and regenerates
# admin_assets_gen.go — the repo-root Go file admin.go's serveAdminPage/
# serveAdminAsset actually serve. Only ever needed after a webui/ source
# change; CI never runs this (admin_assets_gen.go is committed, and
# .gitattributes export-ignores webui/ from release source tarballs — see
# its own comment), so CI never needs node. `npm ci` requires
# webui/package-lock.json, which is committed for exactly this.
admin-ui:
cd webui && npm ci && npm run build
node webui/generate.mjs
gofmt -w admin_assets_gen.go
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 ./...