Skip to content
Draft
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
53 changes: 50 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,62 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
# goreleaser derives the dev version from the latest tag.
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: 1.25
go-version: "1.27"

- name: Build
run: make build

- name: Test
run: make test

- name: Build dev package
uses: goreleaser/goreleaser-action@v7
with:
version: '~> v2'
# The before hooks only repeat the build and test steps above.
args: release --snapshot --clean --skip=before

- name: Upload dev package
uses: actions/upload-artifact@v7
with:
name: bngblasterctrl-dev
path: |
dist/*.deb
dist/checksums.txt
dist/*.tar.gz
if-no-files-found: error

lint:
runs-on: ubuntu-latest
permissions:
contents: read
# only-new-issues reads the pull request diff
pull-requests: read
steps:
- uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v7
with:
go-version: "1.27"

- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.14.0
# The prebuilt v2.14 binaries are built with Go 1.26 and refuse to
# load a module targeting Go 1.27; build it with the Go set up above
# instead. Switch back to the default binary install once a release
# built with Go >= 1.27 is out.
install-mode: goinstall
# The code predates most of the linters "default: all" now enables;
# fail on issues introduced by a change instead of the whole backlog.
only-new-issues: true
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: 1.25
go-version: "1.27"

- name: GoReleaser Action
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
version: '~> v2'
args: release --clean
Expand Down
112 changes: 75 additions & 37 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,43 +1,81 @@
linters-settings:
gci:
sections:
- standard
- default
- prefix(github.com/rtbrick)
godot:
# list of regexps for excluding particular comment lines from check
exclude:
- '@.*'

issues:
exclude-rules:
- linters:
- funlen
- dupl
- bodyclose
- gocritic
- unparam
- lll
path: _test.go

version: "2"
linters:
enable-all: true
default: all
# "all" keeps growing with every golangci-lint release; the linters below
# are pure style preferences this codebase deliberately does not follow
# (or are deprecated, or need per-project config that adds nothing here,
# like depguard).
disable:
- scopelint
- golint
- interfacer
- maligned
- prealloc
- depguard
- err113
- errchkjson
- errorlint
- exhaustruct
- exhaustruct_v5
- funcorder
- gochecknoglobals
- wrapcheck
- testpackage
- godoclint
- gomodguard
- lll
- nestif
- nlreturn
- exhaustivestruct
- wsl
- noinlineerr
- paralleltest
- varnamelen
- goerr113
- prealloc
- tagliatelle
- errorlint
- errchkjson
- nestif
- testpackage
- varnamelen
- wrapcheck
- wsl
- wsl_v5
settings:
ireturn:
# The defaults, plus type parameters: generic containers such as the
# summary cache necessarily return T.
allow:
- anon
- error
- empty
- stdlib
- generic
godot:
exclude:
- '@.*'
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- bodyclose
- dupl
- funlen
- gocritic
- lll
- unparam
path: _test.go
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
settings:
gci:
sections:
- standard
- default
- prefix(github.com/rtbrick)
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
22 changes: 21 additions & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
version: 2

before:
hooks:
# You may remove this if you don't use go modules.
Expand Down Expand Up @@ -28,18 +30,36 @@ nfpms:
- deb
bindir: /usr/local/bin
section: utils
# bngblaster ships as a GitHub release .deb rather than from an apt
# repository, so a hard dependency would make a plain dpkg -i fail.
recommends:
- bngblaster
contents:
- src: debian/scripts/systemd/rtbrick-bngblasterctrl.service
dst: /lib/systemd/system/rtbrick-bngblasterctrl.service
file_info:
mode: 0644
- src: debian/scripts/default/rtbrick-bngblasterctrl
dst: /etc/default/rtbrick-bngblasterctrl
type: config|noreplace
file_info:
mode: 0644
- src: debian/scripts/logrotate/rtbrick-bngblasterctrl
dst: /etc/logrotate.d/rtbrick-bngblasterctrl
type: config|noreplace
file_info:
mode: 0644
scripts:
postinstall: debian/scripts/postinstall.sh
preremove: debian/scripts/preremove.sh
postremove: debian/scripts/postremove.sh
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
# Dev builds from CI. The "~" makes dpkg sort them below the upcoming
# release (0.1.4~dev... < 0.1.4), so installing the release upgrades over
# a dev build; the commit timestamp orders successive dev builds.
version_template: "{{ incpatch .Version }}~dev.{{ .CommitTimestamp }}.{{ .ShortCommit }}"
changelog:
sort: asc
filters:
Expand Down
83 changes: 83 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# CLAUDE.md

REST controller daemon (`bngblasterctrl`) for the [BNG Blaster](https://github.com/rtbrick/bngblaster).
It creates, starts, stops and inspects multiple `bngblaster` test instances on one host and
wraps each instance's JSON-RPC control socket as a REST API.
It also ships an embedded web UI (served on `/`, on by default, disable with `-ui=false`) that runs
on top of that REST API to manage instances from a browser.

## Commands

```sh
make build # -> bin/<os>_<arch>/bngblasterctrl (version from latest git tag)
make test # go test -v -cover ./... (what CI runs, with make build)
go test ./pkg/server -run TestServer_create # single test
make lint # golangci-lint v2 (default: all, see .golangci.yml); CI only fails on new issues
make fumpt # gofumpt formatting
make gci # import order: standard, default, github.com/rtbrick
go generate ./pkg/controller # regenerate repositorymock.go (needs matryer/moq)
```

Run locally without root by pointing at a writable folder:

```sh
./bin/linux_amd64/bngblasterctrl -d /tmp/bngblaster -debug
```

## Layout

- `cmd/bngblasterctrl/` – flag parsing, zerolog setup (warn+ goes to stderr, rest to stdout), HTTP server.
- `pkg/controller/` – instance lifecycle on disk and process management.
- `Repository` interface (`model.go`) is the seam between HTTP and the file system/processes;
`DefaultRepository` (`repository.go`) is the real implementation.
- Each instance is a folder `<config-dir>/<name>/` holding `config.json`, `run.json`, `run.pid`,
`run.sock`, `run.log`, `run_report.json`, `run.pcap`, `run.stdout`, `run.stderr`
(filename constants in `repository.go`). "Running" is derived from these files, not from in-memory state.
- `prom.go` – Prometheus metrics collected from running instances via the control socket.
- `pkg/server/` – gorilla/mux router (`server.go` `routes()`), one file per feature
(streams, sessions, overview, logs, files, ui, apidocs). `cache.go` is a short-TTL,
per-instance summary cache with in-flight dedup; invalidate it on any lifecycle change.
`hardening.go` holds the auth-independent protections (cross-origin check for
state-changing requests, `-allowed-hosts`, security headers/CSP, body size limits); bound every
new request body with `http.MaxBytesReader` and log lifecycle changes via `auditLog`.
- `pkg/server/webui/` – experimental embedded SPA. Vanilla HTML/CSS/JS, **no build step,
no framework, no npm** – files are `go:embed`ed and served as-is. `index.html` is a Go
template (`{{.AssetVersion}}` cache-busting).
- `docs/` – `swagger.yaml` + Swagger UI, embedded into the binary and also published via GitHub Pages.
- `debian/` – systemd unit, `/etc/default` env file, logrotate, install scripts (packaged by goreleaser).

## Conventions

- Every Go file starts with:
```go
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (C) 2020-2026, RtBrick, Inc.
```
- Constructors use functional options (`NewServer(repo, WithUI(...))`, `NewDefaultRepository(WithConfigFolder(...))`).
Optional surface sits behind a flag + option. `-ui`, `-upload` and `-interfaces-api` default to on in the
binary (disable with `-flag=false`); the server/repository options themselves still default to off.
- Handlers are methods returning `http.HandlerFunc`. Always sanitize the instance path variable with
`cleanPathVariable`, and file names with `filepath.Base` + `isUnsafeFileName`. There is no auth yet
(`authMiddleware` is a no-op hook), so path-traversal safety matters.
- Use `JSONError` / `JSONNotFound` for error responses; map `controller.ErrBlaster*` errors to HTTP status
(running → 412, not exists → 404).
- Logging via `github.com/rs/zerolog/log` with structured fields.
- Comments explain *why*; the codebase uses fairly thorough doc comments – match that density.

## Testing

- Server tests use `controller.RepositoryMock` (moq) plus `httpexpect`/`httptest`; table-driven with `testify/require`.
- Process tests fake `bngblaster` via `controller.ExecCommand` and the `TestHelperProcess` /
`GO_WANT_HELPER_PROCESS` pattern (`process_test.go`).
- Fixtures live in `pkg/controller/td/`.
- After changing the `Repository` interface, regenerate the mock or the build breaks.

## When changing the API

Update in the same change: the route in `server.go`, `docs/swagger.yaml`, the web UI (`app.js`) if it
consumes the endpoint, and the README if flags or defaults change.

## Release

Tag-driven via goreleaser (`.goreleaser.yaml`, `.github/workflows/release.yml`): linux/amd64 static
binary (`CGO_ENABLED=0`) + `.deb`. `main.Version` is injected through ldflags.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
BSD 3-Clause License

Copyright (C) 2020-2025, RtBrick, Inc.
Copyright (C) 2020-2026, RtBrick, Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without
Expand Down
Loading
Loading