Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
927ee136a9 |
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
|
backend/
|
||||||
dist/
|
dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
tmp/
|
tmp/
|
||||||
yarn.lock
|
yarn.lock
|
||||||
.claude/
|
.claude/
|
||||||
# The org standard file, copied verbatim; backend/script/lint checks its sha256.
|
|
||||||
backend/.golangci.yml
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.PHONY: bootstrap setup dev build test lint fmt fmt-check check \
|
.PHONY: bootstrap setup dev test lint fmt fmt-check check frontend-check \
|
||||||
frontend-check frontend-viewport-test docker hooks
|
frontend-viewport-test docker hooks
|
||||||
|
|
||||||
# Standard targets are thin shims; the implementations live in script/
|
# Standard targets are thin shims; the implementations live in script/
|
||||||
# per the scripts-to-rule-them-all pattern (see the Entrypoints section
|
# per the scripts-to-rule-them-all pattern (see the Entrypoints section
|
||||||
@@ -13,11 +13,7 @@ setup:
|
|||||||
@script/setup
|
@script/setup
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
@script/dev
|
yarn dev
|
||||||
|
|
||||||
# The frontend only; backend/Makefile's build target builds the Go server.
|
|
||||||
build:
|
|
||||||
@script/build
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@script/test
|
@script/test
|
||||||
|
|||||||
@@ -46,10 +46,6 @@ halves, so the root `make check` fails if either one is broken. We provide:
|
|||||||
both linters in Docker
|
both linters in Docker
|
||||||
- `script/setup` — make a fresh clone ready for development: bootstrap plus the
|
- `script/setup` — make a fresh clone ready for development: bootstrap plus the
|
||||||
git pre-commit hook
|
git pre-commit hook
|
||||||
- `script/dev` — run the Vite dev server, which proxies `/api` to a locally
|
|
||||||
running `netwatch-server`
|
|
||||||
- `script/build` — build the frontend for production into `dist/`;
|
|
||||||
`backend/script/build` builds the Go server
|
|
||||||
- `script/projectname` — print the project name (used for the Docker image tag)
|
- `script/projectname` — print the project name (used for the Docker image tag)
|
||||||
- `script/test` — run `script/frontend-test`, then `backend/script/test`, the
|
- `script/test` — run `script/frontend-test`, then `backend/script/test`, the
|
||||||
backend's Go tests with the race detector and coverage
|
backend's Go tests with the race detector and coverage
|
||||||
@@ -65,8 +61,7 @@ halves, so the root `make check` fails if either one is broken. We provide:
|
|||||||
- `script/frontend-lint` — run eslint with the rules in `eslint.config.js`; it
|
- `script/frontend-lint` — run eslint with the rules in `eslint.config.js`; it
|
||||||
runs inside the `frontend-lint` stage of `Dockerfile`, which `make lint`
|
runs inside the `frontend-lint` stage of `Dockerfile`, which `make lint`
|
||||||
builds
|
builds
|
||||||
- `script/frontend-fmt` — format everything prettier understands (writes), the
|
- `script/frontend-fmt` — format everything prettier understands (writes)
|
||||||
markdown in `backend/` included
|
|
||||||
- `script/frontend-fmt-check` — check prettier formatting (read-only)
|
- `script/frontend-fmt-check` — check prettier formatting (read-only)
|
||||||
- `script/frontend-check` — run `script/frontend-test` and
|
- `script/frontend-check` — run `script/frontend-test` and
|
||||||
`script/frontend-fmt-check`, for the frontend stage of `Dockerfile`, which has
|
`script/frontend-fmt-check`, for the frontend stage of `Dockerfile`, which has
|
||||||
@@ -255,8 +250,8 @@ What the [upaas](https://git.eeqj.de/sneak/upaas) app for netwatch needs:
|
|||||||
the backend records Prometheus metrics of its requests and serves them at
|
the backend records Prometheus metrics of its requests and serves them at
|
||||||
`/metrics` on the container port, to requests with this user name and
|
`/metrics` on the container port, to requests with this user name and
|
||||||
password as their basic auth credentials. With neither set, there are no
|
password as their basic auth credentials. With neither set, there are no
|
||||||
metrics and `/metrics` is not found. One set without the other, or a user
|
metrics and `/metrics` is not found. One set without the other stops the
|
||||||
name containing `:`, stops the container
|
container
|
||||||
- **Health check:** the image's `HEALTHCHECK` requests
|
- **Health check:** the image's `HEALTHCHECK` requests
|
||||||
`/.well-known/healthcheck` through nginx every 30 seconds, so it fails unless
|
`/.well-known/healthcheck` through nginx every 30 seconds, so it fails unless
|
||||||
both nginx and the backend answer. upaas reads the container's health 60
|
both nginx and the backend answer. upaas reads the container's health 60
|
||||||
|
|||||||
@@ -29,23 +29,12 @@ latest run passes.
|
|||||||
Go's runtime and process metrics, at `GET /metrics` behind basic auth with
|
Go's runtime and process metrics, at `GET /metrics` behind basic auth with
|
||||||
those credentials; nginx passes `/metrics` to it as it does `/api/`. With
|
those credentials; nginx passes `/metrics` to it as it does `/api/`. With
|
||||||
neither set there are no metrics and `/metrics` is 404; one without the other
|
neither set there are no metrics and `/metrics` is 404; one without the other
|
||||||
stops the start with an error naming both, and so does a `METRICS_USERNAME`
|
stops the start with an error naming both. Only requests that reach the health
|
||||||
containing `:`, with an error naming it. Only requests that reach the health
|
|
||||||
check or `POST /api/v1/reports` are recorded, not `/metrics` itself and not
|
check or `POST /api/v1/reports` are recorded, not `/metrics` itself and not
|
||||||
every request as `GO_HTTP_SERVER_CONVENTIONS.md` shows, because the labels are
|
every request as `GO_HTTP_SERVER_CONVENTIONS.md` shows, because the labels are
|
||||||
the request's path and method, which clients can make up without end. For
|
the request's path and method, which clients can make up without end. For
|
||||||
that, `POST /api/v1/reports` is now registered by its full path instead of
|
that, `POST /api/v1/reports` is now registered by its full path instead of
|
||||||
inside a `/api/v1` route group; it answers as before
|
inside a `/api/v1` route group; it answers as before
|
||||||
- 2026-10-04: `script/` and `Makefile` follow the org models (issue #28):
|
|
||||||
`make dev` shims to the new `script/dev`, the Vite dev server, and the new
|
|
||||||
`make build` to `script/build`, the frontend production build.
|
|
||||||
`.prettierignore` no longer leaves out `backend/`, so `make fmt` and
|
|
||||||
`make fmt-check` cover `backend/README.md`; it leaves out
|
|
||||||
`backend/.golangci.yml` by name, the org standard file whose sha256
|
|
||||||
`backend/script/lint` checks. `script/install-precommit` and the date on
|
|
||||||
`script/bootstrap`'s pins are the org model again; `script/bootstrap`,
|
|
||||||
`script/fmt` and `script/fmt-check` each say in a comment why they differ from
|
|
||||||
it
|
|
||||||
- 2026-10-04: a frontend build on Node 26 or newer, such as `make test` on a
|
- 2026-10-04: a frontend build on Node 26 or newer, such as `make test` on a
|
||||||
host with Node 26, no longer prints Node's warning that `module.register()` is
|
host with Node 26, no longer prints Node's warning that `module.register()` is
|
||||||
deprecated (issue #32); the build in `Dockerfile` runs on Node 22, which never
|
deprecated (issue #32); the build in `Dockerfile` runs on Node 22, which never
|
||||||
|
|||||||
+20
-23
@@ -32,16 +32,17 @@ pattern as the repo root: the targets in `backend/Makefile` are thin shims over
|
|||||||
`test`, `fmt` and `fmt-check`:
|
`test`, `fmt` and `fmt-check`:
|
||||||
|
|
||||||
- `script/build` — compile the static `netwatch-server` binary with its version
|
- `script/build` — compile the static `netwatch-server` binary with its version
|
||||||
stamped in. The version is `VERSION` from the environment; when that is unset
|
stamped in. The version is `VERSION` from the environment;
|
||||||
or empty, it falls back to `git describe` inside a git checkout, then to `dev`
|
when that is unset or empty, it falls back to `git describe` inside a git
|
||||||
|
checkout, then to `dev`
|
||||||
- `script/test` — run the Go tests with the race detector and coverage. Go's
|
- `script/test` — run the Go tests with the race detector and coverage. Go's
|
||||||
`-timeout 30s` bounds the tests, not their compile. If they fail, they run
|
`-timeout 30s` bounds the tests, not their compile. If they fail, they run
|
||||||
again with `-v` for the details, and the script fails. The race detector needs
|
again with `-v` for the details, and the script fails. The race detector needs
|
||||||
a C compiler
|
a C compiler
|
||||||
- `script/lint` — check `.golangci.yml` against its pinned sha256, then run
|
- `script/lint` — check `.golangci.yml` against its pinned sha256, then run
|
||||||
golangci-lint. It runs inside the golangci-lint image of the lint stage of the
|
golangci-lint. It runs inside the golangci-lint image of the lint stage of
|
||||||
root `Dockerfile`; from a checkout, run `make lint` at the repo root, which
|
the root `Dockerfile`; from a checkout, run `make lint` at the repo root,
|
||||||
builds that stage
|
which builds that stage
|
||||||
- `script/fmt` — format the Go sources (writes)
|
- `script/fmt` — format the Go sources (writes)
|
||||||
- `script/fmt-check` — check Go formatting (read-only)
|
- `script/fmt-check` — check Go formatting (read-only)
|
||||||
- `script/run` — build and run the server locally
|
- `script/run` — build and run the server locally
|
||||||
@@ -60,9 +61,8 @@ flushes them to compressed files on disk for later analysis.
|
|||||||
|
|
||||||
## Design
|
## Design
|
||||||
|
|
||||||
The server is structured as an `fx`-wired Go application under
|
The server is structured as an `fx`-wired Go application under `cmd/netwatch-server/`.
|
||||||
`cmd/netwatch-server/`. Internal packages in `internal/` follow standard Go
|
Internal packages in `internal/` follow standard Go project layout:
|
||||||
project layout:
|
|
||||||
|
|
||||||
- **`config`**: Loads configuration from environment variables and config files
|
- **`config`**: Loads configuration from environment variables and config files
|
||||||
via Viper.
|
via Viper.
|
||||||
@@ -91,12 +91,11 @@ project layout:
|
|||||||
| `METRICS_USERNAME` | empty | Basic auth user name for `/metrics`; see [Metrics](#metrics) |
|
| `METRICS_USERNAME` | empty | Basic auth user name for `/metrics`; see [Metrics](#metrics) |
|
||||||
| `METRICS_PASSWORD` | empty | Basic auth password for `/metrics`; see [Metrics](#metrics) |
|
| `METRICS_PASSWORD` | empty | Basic auth password for `/metrics`; see [Metrics](#metrics) |
|
||||||
|
|
||||||
`TRUSTED_PROXIES` defaults to
|
`TRUSTED_PROXIES` defaults to `127.0.0.1/32,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16`.
|
||||||
`127.0.0.1/32,::1/128,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16`. The loopback
|
The loopback entries cover a reverse proxy on the same host. A request whose
|
||||||
entries cover a reverse proxy on the same host. A request whose direct peer is
|
direct peer is outside this set has its forwarded headers ignored, and the
|
||||||
outside this set has its forwarded headers ignored, and the direct peer is
|
direct peer is logged and rate-limited instead. The container image does not use
|
||||||
logged and rate-limited instead. The container image does not use this default;
|
this default; see [Container image](#container-image).
|
||||||
see [Container image](#container-image).
|
|
||||||
|
|
||||||
A variable set to a value the server cannot use, such as `PORT=abc`,
|
A variable set to a value the server cannot use, such as `PORT=abc`,
|
||||||
`DEBUG=maybe` or a `BIND_ADDRESS` that is not an IP address, stops it from
|
`DEBUG=maybe` or a `BIND_ADDRESS` that is not an IP address, stops it from
|
||||||
@@ -165,7 +164,8 @@ credentials, so it is bounded instead. Both refusals below answer with the same
|
|||||||
to be written fill the cap on their own, and then no file is deleted. At
|
to be written fill the cap on their own, and then no file is deleted. At
|
||||||
start, report files past the cap, as after lowering it, are deleted the same
|
start, report files past the cap, as after lowering it, are deleted the same
|
||||||
way. So the cap is how much of the newest reports is kept: the default of 1
|
way. So the cap is how much of the newest reports is kept: the default of 1
|
||||||
GiB is small enough for any host; set it to the space you can give `DATA_DIR`.
|
GiB is small enough for any host; set it to the space you can give
|
||||||
|
`DATA_DIR`.
|
||||||
|
|
||||||
### CORS
|
### CORS
|
||||||
|
|
||||||
@@ -189,14 +189,11 @@ method and status; they also count those requests in progress, and include Go's
|
|||||||
runtime and process metrics. No other request is recorded: not those to
|
runtime and process metrics. No other request is recorded: not those to
|
||||||
`/metrics` itself, and not those answered before they reach either route, such
|
`/metrics` itself, and not those answered before they reach either route, such
|
||||||
as a CORS preflight, or a request refused with 404 for a path no route has, 405
|
as a CORS preflight, or a request refused with 404 for a path no route has, 405
|
||||||
for a method its route does not take, or 413 for declaring a body length over
|
for a method its route does not take, or 413 for a body over the 1 MiB limit.
|
||||||
the 1 MiB limit. A report whose body goes over the limit without declaring its
|
Clients can make up any number of paths and methods, and each would add labels
|
||||||
length reaches the route, is answered 413 there, and is recorded with that
|
to the metrics for as long as the server runs. With neither set, nothing is
|
||||||
status. Clients can make up any number of paths and methods, and each would add
|
recorded and `/metrics` answers 404. One without the other stops the server
|
||||||
labels to the metrics for as long as the server runs. With neither set, nothing
|
from starting, with an error naming both.
|
||||||
is recorded and `/metrics` answers 404. One without the other stops the server
|
|
||||||
from starting, with an error naming both; so does a `METRICS_USERNAME`
|
|
||||||
containing `:`, which basic auth cannot carry, with an error naming it.
|
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,6 @@ var (
|
|||||||
"METRICS_USERNAME and METRICS_PASSWORD must be set together, " +
|
"METRICS_USERNAME and METRICS_PASSWORD must be set together, " +
|
||||||
"or neither",
|
"or neither",
|
||||||
)
|
)
|
||||||
errMetricsUsernameColon = errors.New(
|
|
||||||
"METRICS_USERNAME must not contain \":\", " +
|
|
||||||
"which basic auth cannot carry in a user name",
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Params defines the dependencies for Config.
|
// Params defines the dependencies for Config.
|
||||||
@@ -193,12 +189,6 @@ func (s *Config) check() error {
|
|||||||
return errMetricsCredentials
|
return errMetricsCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic auth splits the credentials at the first ":", so with one
|
|
||||||
// in the user name every request to /metrics would get 401.
|
|
||||||
if strings.Contains(s.MetricsUsername, ":") {
|
|
||||||
return errMetricsUsernameColon
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkOrigins(s.CORSAllowedOrigins)
|
return checkOrigins(s.CORSAllowedOrigins)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,16 +119,6 @@ func TestMetricsCredentialsGoTogether(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMetricsUsernameMustNotContainColon: basic auth splits the
|
|
||||||
// credentials at the first ":", so such a user name would get 401 on
|
|
||||||
// every request to /metrics.
|
|
||||||
func TestMetricsUsernameMustNotContainColon(t *testing.T) {
|
|
||||||
t.Setenv("METRICS_USERNAME", "prom:etheus")
|
|
||||||
t.Setenv("METRICS_PASSWORD", "secret")
|
|
||||||
|
|
||||||
requireConfigError(t, "METRICS_USERNAME")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCORSAllowedOriginsMustBeOrigins: "*" would let every origin in,
|
// TestCORSAllowedOriginsMustBeOrigins: "*" would let every origin in,
|
||||||
// and an entry that is not a plain origin would match no page.
|
// and an entry that is not a plain origin would match no page.
|
||||||
func TestCORSAllowedOriginsMustBeOrigins(t *testing.T) {
|
func TestCORSAllowedOriginsMustBeOrigins(t *testing.T) {
|
||||||
|
|||||||
+1
-3
@@ -17,13 +17,11 @@
|
|||||||
#
|
#
|
||||||
# golangci-lint is not installed: make lint runs it in Docker, which
|
# golangci-lint is not installed: make lint runs it in Docker, which
|
||||||
# this script does not install either.
|
# this script does not install either.
|
||||||
#
|
|
||||||
# Unlike the org model: Go and gcc for backend/, a newer node for eslint.
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
# Pinned versions, 2026-07-06
|
# Pinned versions, 2026-07-07
|
||||||
NODE_VERSION="22.17.0"
|
NODE_VERSION="22.17.0"
|
||||||
# The oldest node the frontend's dependencies accept: the "engines"
|
# The oldest node the frontend's dependencies accept: the "engines"
|
||||||
# field of eslint 10.12.0, the most demanding of them, asks for 22.13.0
|
# field of eslint 10.12.0, the most demanding of them, asks for 22.13.0
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/build: build the frontend for production into dist/. The Go
|
|
||||||
# backend is built by backend/script/build.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
yarn build
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
-13
@@ -1,13 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/dev: run the frontend's Vite dev server. It proxies /api to a
|
|
||||||
# netwatch-server running locally (see vite.config.js).
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
yarn dev
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/fmt: format the whole repo (writes): prettier over everything
|
# script/fmt: format the whole repo (writes): prettier over everything
|
||||||
# it understands, then gofmt over the Go backend.
|
# it understands, then gofmt over the Go backend.
|
||||||
# The org model formats only markdown; this repo also has JS and Go.
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/fmt-check: check formatting across the whole repo (read-only).
|
# script/fmt-check: check formatting across the whole repo (read-only).
|
||||||
# Same scope as script/fmt, but fails instead of writing.
|
# Same scope as script/fmt, but fails instead of writing.
|
||||||
# The org model checks only markdown; this repo also has JS and Go.
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/frontend-fmt: format the frontend and every other file prettier
|
# script/frontend-fmt: format the frontend and every other file prettier
|
||||||
# understands, repo-wide (writes), the markdown in backend/ included.
|
# understands, repo-wide (writes). backend/ is in .prettierignore; Go
|
||||||
# Prettier does not read Go; backend/script/fmt formats the Go sources.
|
# sources are formatted by backend/script/fmt.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
hook=".git/hooks/pre-commit"
|
hook=".git/hooks/pre-commit"
|
||||||
printf '#!/bin/sh\nset -e\nscript/precommit\n' > .git/hooks/pre-commit
|
printf '#!/bin/sh\nset -e\nscript/precommit\n' > "$hook"
|
||||||
chmod +x .git/hooks/pre-commit
|
chmod +x "$hook"
|
||||||
echo "pre-commit hook installed: runs script/precommit"
|
echo "pre-commit hook installed: runs script/precommit"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user