Development
Day-to-day build, test, vet, lint, and proto workflow. All targets are
in the top-level Makefile; the linter is configured
by .golangci.yml.
Toolchain
- Go — pinned by
go.mod. CI usessetup-go@v5withgo-version-file: go.mod. Match locally with whatever pins the same version (gvm,goenv,asdf, or just install the matching tag). - buf — used by
make proto. Install per https://buf.build/docs/installation. - nfpm — used by
make package. Install withgo install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.46.3. - Docker — required for runtime tests and the isolation rig.
- nftables + wireguard-tools — for the integration / isolation paths.
Make targets
| target | runs |
|---|---|
make build | go build -o jaco ./cmd/jaco |
make test | go test ./... -race -count=1 |
make ci-test | mirrors CI: -race -coverprofile -skip <known flake> |
make test-isolation | runs scripts/test/isolation-rig.sh (privileged) |
make vet | go vet ./... |
make lint | vet + gofmt -l check |
make proto | buf generate (regenerates pkg/proto/jaco/v1/) |
make package | builds .deb/.rpm/.apk locally via nfpm |
make release | cross-builds linux + darwin × amd64 + arm64 tarballs |
make clean | removes ./jaco and dist/ |
make ci-test skips TestExportImport_RoundTripPreservesBootstrapToken —
a known snapshot-rename timestamp-collision flake tracked separately.
Working with internal/
Subsystems are wired with explicit dependencies, no global state, no
init() magic:
- Loggers are passed in. Never reach for
slog.Default()from a subsystem. The package's own logger comes via constructor (or a field); derive children withlogging.Subsystem(base, "name"). See Observability andinternal/logging/. - Watches are subscribed via
internal/controlplane/watch. Buffered channels with drop-newest-on-overflow; on overflow you get a syntheticResyncevent so subscribers re-fetch full state. - Raft writes route through the leader. Non-leader handlers
forward via
Internal.Submit. Do not callraft.Applydirectly from a follower path. - Scheduler-side code must self-gate on
leader.IsLeader(). Subsystems that run on every node (runtime, ingress, discovery) do not need the gate.
Adding a CLI subcommand
The CLI follows a consistent shape; copy
cmd/jaco/apply.go as a template:
cmd <name>Cmd() *cobra.Commandbuilding the cobra command with flags.RunEreads flags, callsdialOperator(...)to get a connection + auth decorator, sets a context deadline appropriate for the call.- A
runX(ctx, client, ..., out io.Writer) errorbody taking the proto client so unit tests can inject a fake. - Add the command to the root in an
init()block.
Follow the same flag set every operator command uses: --server,
--token, --ca-cert, --socket. Use defaultCACertPath() and
socketDefault() for the defaults.
Adding a gRPC handler
- Edit
proto/jaco/v1/services.proto(orentities.protofor new message types). make proto. Commit the regeneratedpkg/proto/jaco/v1/alongside the source.- Implement the handler under
internal/controlplane/grpc/<file>.go. - If the call mutates state, the handler builds a
Command{}proto and routes it through raft (raft.Applyon the leader,Internal.Submitforwarded from a follower). - Add an admission rule under
internal/controlplane/admission/if the call needs anything other than the default token gate. - Write a unit test in the same package + an integration test under
internal/controlplane/grpc/if the call has cross-vertical effects.
Linting expectations
Correctness-only linters (errcheck, govet, ineffassign,
staticcheck, unused). Style linters are intentionally off:
gofmtis enforced separately bymake lint'sgofmt -lcheck. Rungofmt -w .before pushing.- Naming, capitalization, and comment-style suggestions from
staticcheckare disabled — no value vs. churn. golangci-lintv2 schema. Pin matches CI'sv2.12.2(the first release built with go1.25, which the module pins).
Per-file or per-rule exemptions live in .golangci.yml::issues.exclusions.
Branch hygiene
- One logical change per PR. Keep generated proto changes in their
own commit (
make proto). - Run
make ci-test vet lintlocally before pushing. make test-isolationrequires CAP_NET_ADMIN + CAP_NET_RAW + kernel WG + nftables + docker. CI runs it under a privileged runner; locally setJACO_RIG_FORCE=1once you've confirmed the host has what it needs.