cmd/netwatch-server/main.go parsed the command line, ran the check-cidr and prepare-data-dir subcommands and built the fx application itself; the Go style guide puts all of that in internal/ or pkg/. The new package internal/cli does it, with the same arguments, messages and exit codes. main() is now os.Exit(cli.Main(Appname, Version, os.Args[1:], cli.RunServer)): Main takes the function that runs the server, so its tests call it with one that fails them. Appname and Version stay in main, where the build sets Version. Each subcommand writes its error to a writer it is given, and prepare-data-dir also takes the volume and the user's name, so its own tests give it a temporary directory and the user they run as. Model: opus-5-5
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
// Package cli runs netwatch-server from its command line: the
|
||||
// check-cidr and prepare-data-dir subcommands, which bin/entrypoint.sh
|
||||
// runs before it starts the server, and otherwise the server, with the
|
||||
// fx wiring.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
|
||||
"sneak.berlin/go/netwatch/internal/config"
|
||||
"sneak.berlin/go/netwatch/internal/globals"
|
||||
"sneak.berlin/go/netwatch/internal/handlers"
|
||||
"sneak.berlin/go/netwatch/internal/healthcheck"
|
||||
"sneak.berlin/go/netwatch/internal/logger"
|
||||
"sneak.berlin/go/netwatch/internal/middleware"
|
||||
"sneak.berlin/go/netwatch/internal/reportbuf"
|
||||
"sneak.berlin/go/netwatch/internal/server"
|
||||
|
||||
"go.uber.org/fx"
|
||||
"go.uber.org/fx/fxevent"
|
||||
)
|
||||
|
||||
// Main runs netwatch-server with args, its command line after the
|
||||
// program's name, and returns the exit code. Unless args are
|
||||
// "check-cidr CIDR" or "prepare-data-dir DATA_DIR", Main runs the
|
||||
// server by calling serve with appname and version, the name and
|
||||
// version the server logs and reports. main passes RunServer; the
|
||||
// tests pass a function that fails them, so that no test starts the
|
||||
// server. When serve is RunServer and the server fails to start or to
|
||||
// stop, fx's Run exits the process with status 1 and Main never
|
||||
// returns.
|
||||
func Main(
|
||||
appname, version string,
|
||||
args []string,
|
||||
serve func(appname, version string),
|
||||
) int {
|
||||
// "netwatch-server check-cidr CIDR" exits 1, with the error, if
|
||||
// this server would refuse CIDR in its TRUSTED_PROXIES.
|
||||
// bin/entrypoint.sh runs it on each entry it gives nginx.
|
||||
if len(args) == 2 && args[0] == "check-cidr" {
|
||||
return checkCIDR(os.Stderr, args[1])
|
||||
}
|
||||
|
||||
// "netwatch-server prepare-data-dir DATA_DIR" gets DATA_DIR ready
|
||||
// for the netwatch user, or exits 1 with the error; see
|
||||
// reportbuf.PrepareDataDir. bin/entrypoint.sh runs it as root
|
||||
// before it starts this server as that user.
|
||||
if len(args) == 2 && args[0] == "prepare-data-dir" {
|
||||
return prepareDataDir(os.Stderr, "/data", args[1], "netwatch")
|
||||
}
|
||||
|
||||
serve(appname, version)
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// checkCIDR returns 0 if the server would take cidr in its
|
||||
// TRUSTED_PROXIES, and otherwise writes the error to stderr and
|
||||
// returns 1.
|
||||
func checkCIDR(stderr io.Writer, cidr string) int {
|
||||
_, err := middleware.ParseTrustedProxies([]string{cidr})
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(stderr, err)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// prepareDataDir gets dataDir ready for the user named username with
|
||||
// reportbuf.PrepareDataDir, volume being where the data volume is
|
||||
// mounted, and returns 0. Otherwise it writes the error to stderr and
|
||||
// returns 1.
|
||||
func prepareDataDir(stderr io.Writer, volume, dataDir, username string) int {
|
||||
owner, err := user.Lookup(username)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(stderr, err)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
err = reportbuf.PrepareDataDir(volume, dataDir, owner)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(stderr, "DATA_DIR '%s': %v\n", dataDir, err)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// RunServer runs the server until it is told to stop, with appname and
|
||||
// version as the name and version it logs and reports. If the server
|
||||
// fails to start or to stop, fx's Run exits the process with status 1
|
||||
// itself.
|
||||
func RunServer(appname, version string) {
|
||||
globals.Appname = appname
|
||||
globals.Version = version
|
||||
|
||||
fx.New(
|
||||
// fx logs each step of starting and stopping through the
|
||||
// server's own logger, so off a terminal those lines are
|
||||
// JSON like every other line.
|
||||
fx.WithLogger(func(log *logger.Logger) fxevent.Logger {
|
||||
return &fxevent.SlogLogger{Logger: log.Get()}
|
||||
}),
|
||||
fx.Provide(
|
||||
config.New,
|
||||
globals.New,
|
||||
handlers.New,
|
||||
healthcheck.New,
|
||||
logger.New,
|
||||
middleware.New,
|
||||
reportbuf.New,
|
||||
server.New,
|
||||
),
|
||||
fx.Invoke(
|
||||
// First, so the name and version are logged even when
|
||||
// a setting stops the start.
|
||||
func(log *logger.Logger) { log.Identify() },
|
||||
func(*server.Server) {},
|
||||
),
|
||||
).Run()
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/netwatch/internal/cli"
|
||||
"sneak.berlin/go/netwatch/internal/reportbuf"
|
||||
)
|
||||
|
||||
// TestMainRunsCheckCIDR: Main given check-cidr returns 0 for a CIDR the
|
||||
// server takes in its TRUSTED_PROXIES and 1 for one it refuses, and
|
||||
// does not start the server.
|
||||
func TestMainRunsCheckCIDR(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for cidr, want := range map[string]int{"10.0.0.0/8": 0, "1.2.3/32": 1} {
|
||||
code := cli.Main("netwatch", "test", []string{"check-cidr", cidr},
|
||||
failIfServerStarts(t))
|
||||
if code != want {
|
||||
t.Errorf("check-cidr %q: exit %d, want %d", cidr, code, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMainRunsPrepareDataDir: Main given prepare-data-dir returns 1 for
|
||||
// a DATA_DIR outside /data, and does not start the server. The test
|
||||
// image has no netwatch user, so there the 1 comes from looking that
|
||||
// user up; TestPrepareDataDirRefusesPathOutsideVolume checks the path.
|
||||
func TestMainRunsPrepareDataDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
code := cli.Main("netwatch", "test", []string{"prepare-data-dir", "/etc"},
|
||||
failIfServerStarts(t))
|
||||
if code != 1 {
|
||||
t.Errorf("prepare-data-dir /etc: exit %d, want 1", code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckCIDRTakesCIDR: check-cidr exits 0, writing nothing, for a
|
||||
// CIDR the server takes in its TRUSTED_PROXIES.
|
||||
func TestCheckCIDRTakesCIDR(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, cidr := range []string{"10.0.0.0/8", "192.0.2.1/32", "2001:db8::/32"} {
|
||||
var stderr bytes.Buffer
|
||||
|
||||
code := cli.CheckCIDR(&stderr, cidr)
|
||||
if code != 0 || stderr.Len() != 0 {
|
||||
t.Errorf("%q: exit %d, stderr %q; want exit 0 and no output",
|
||||
cidr, code, stderr.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckCIDRRefusesOtherInput: check-cidr exits 1, with an error
|
||||
// naming TRUSTED_PROXIES and the input, for anything the server would
|
||||
// refuse in its TRUSTED_PROXIES, such as 1.2.3/32, which nginx would
|
||||
// look up as a hostname.
|
||||
func TestCheckCIDRRefusesOtherInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, cidr := range []string{
|
||||
"1.2.3/32", "example.com/32", "10.0.0.0/33", "10.0.0.1", "",
|
||||
} {
|
||||
var stderr bytes.Buffer
|
||||
|
||||
code := cli.CheckCIDR(&stderr, cidr)
|
||||
|
||||
want := "TRUSTED_PROXIES " + strconv.Quote(cidr) + ": "
|
||||
if code != 1 || !strings.HasPrefix(stderr.String(), want) {
|
||||
t.Errorf("%q: exit %d, stderr %q; want exit 1 and %q...",
|
||||
cidr, code, stderr.String(), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrepareDataDirTakesPathInVolume: prepare-data-dir exits 0,
|
||||
// writing nothing, for a DATA_DIR below the volume, and creates it.
|
||||
func TestPrepareDataDirTakesPathInVolume(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
volume := t.TempDir()
|
||||
dataDir := filepath.Join(volume, "reports")
|
||||
|
||||
var stderr bytes.Buffer
|
||||
|
||||
code := cli.PrepareDataDir(&stderr, volume, dataDir, currentUsername(t))
|
||||
if code != 0 || stderr.Len() != 0 {
|
||||
t.Errorf("exit %d, stderr %q; want exit 0 and no output",
|
||||
code, stderr.String())
|
||||
}
|
||||
|
||||
info, err := os.Stat(dataDir)
|
||||
if err != nil || !info.IsDir() {
|
||||
t.Errorf("DATA_DIR not created: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrepareDataDirRefusesPathOutsideVolume: prepare-data-dir exits
|
||||
// 1, with an error naming DATA_DIR and its value, for a DATA_DIR
|
||||
// outside the volume.
|
||||
func TestPrepareDataDirRefusesPathOutsideVolume(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
volume := t.TempDir()
|
||||
|
||||
var stderr bytes.Buffer
|
||||
|
||||
code := cli.PrepareDataDir(&stderr, volume, "/etc", currentUsername(t))
|
||||
|
||||
want := "DATA_DIR '/etc': " + reportbuf.ErrDataDirOutsideVolume.Error() + "\n"
|
||||
if code != 1 || stderr.String() != want {
|
||||
t.Errorf("exit %d, stderr %q; want exit 1 and %q",
|
||||
code, stderr.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
// currentUsername is the name of the user the test runs as, the only
|
||||
// owner a test not run as root can give files to.
|
||||
func currentUsername(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
current, err := user.Current()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return current.Username
|
||||
}
|
||||
|
||||
// failIfServerStarts is what the tests give Main in place of
|
||||
// cli.RunServer: it fails the test instead of starting the server.
|
||||
func failIfServerStarts(t *testing.T) func(appname, version string) {
|
||||
t.Helper()
|
||||
|
||||
return func(string, string) {
|
||||
t.Error("Main started the server")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cli
|
||||
|
||||
import "io"
|
||||
|
||||
// CheckCIDR exposes the check-cidr subcommand to the external tests,
|
||||
// which read what it writes to stderr.
|
||||
func CheckCIDR(stderr io.Writer, cidr string) int {
|
||||
return checkCIDR(stderr, cidr)
|
||||
}
|
||||
|
||||
// PrepareDataDir exposes the prepare-data-dir subcommand to the
|
||||
// external tests, which give it a volume and a user of their own in
|
||||
// place of /data and netwatch.
|
||||
func PrepareDataDir(stderr io.Writer, volume, dataDir, username string) int {
|
||||
return prepareDataDir(stderr, volume, dataDir, username)
|
||||
}
|
||||
Reference in New Issue
Block a user