check / check (push) Canceled after 0s
fx's event lines, errors included, now log at debug level, so a normal start shows none and DEBUG shows them. RunServer starts and stops the fx app itself instead of calling Run: when the start fails, it logs the error's root cause as one "cannot start" line, the backend's last, and returns 1. Main returns the server's exit code. Model: opus-5-5
190 lines
5.5 KiB
Go
190 lines
5.5 KiB
Go
// 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, given no arguments, the
|
|
// server, with the fx wiring.
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"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/dig"
|
|
"go.uber.org/fx"
|
|
"go.uber.org/fx/fxevent"
|
|
)
|
|
|
|
// usageExitCode is what Main returns for a command line it does not
|
|
// understand, the usual exit code for a usage error.
|
|
const usageExitCode = 2
|
|
|
|
// Main runs netwatch-server with args, its command line after the
|
|
// program's name, and returns the exit code. With no args, Main runs
|
|
// the server by calling serve with appname and version, the name and
|
|
// version the server logs and reports, and returns serve's exit code.
|
|
// With "check-cidr CIDR" or "prepare-data-dir DATA_DIR" it runs that
|
|
// subcommand, which writes its errors to stderr. Any other args get a
|
|
// one-line usage message on stderr and exit code 2. main passes
|
|
// os.Stderr and RunServer; the tests pass a buffer and a function of
|
|
// their own, so that no test starts the server.
|
|
func Main(
|
|
stderr io.Writer,
|
|
appname, version string,
|
|
args []string,
|
|
serve func(appname, version string) int,
|
|
) 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(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(stderr, "/data", args[1], "netwatch")
|
|
}
|
|
|
|
// Any other command line is refused, so that a mistyped one does
|
|
// not start the server.
|
|
if len(args) > 0 {
|
|
_, _ = fmt.Fprintln(stderr,
|
|
"usage: netwatch-server [check-cidr CIDR | prepare-data-dir DATA_DIR]")
|
|
|
|
return usageExitCode
|
|
}
|
|
|
|
return serve(appname, version)
|
|
}
|
|
|
|
// 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, and returns the
|
|
// exit code. If the server fails to start, to listen or to stop, it
|
|
// returns 1. A failed start logs one line with the error's cause, such
|
|
// as the setting refused and why, as the server's last line.
|
|
func RunServer(appname, version string) int {
|
|
globals.Appname = appname
|
|
globals.Version = version
|
|
|
|
var log *logger.Logger
|
|
|
|
app := fx.New(
|
|
// fx logs each step of starting and stopping, errors included,
|
|
// at debug level through the server's own logger: they show
|
|
// only with DEBUG set, and off a terminal they are JSON like
|
|
// every other line.
|
|
fx.WithLogger(func(log *logger.Logger) fxevent.Logger {
|
|
fxLog := &fxevent.SlogLogger{Logger: log.Get()}
|
|
fxLog.UseLogLevel(slog.LevelDebug)
|
|
fxLog.UseErrorLevel(slog.LevelDebug)
|
|
|
|
return fxLog
|
|
}),
|
|
fx.Provide(
|
|
config.New,
|
|
globals.New,
|
|
handlers.New,
|
|
healthcheck.New,
|
|
logger.New,
|
|
middleware.New,
|
|
reportbuf.New,
|
|
server.New,
|
|
),
|
|
// First, so that log is set for the error below even when a
|
|
// setting stops the start.
|
|
fx.Populate(&log),
|
|
fx.Invoke(
|
|
// Before the server, so that the name and version are
|
|
// logged even when a setting stops the start.
|
|
func(log *logger.Logger) { log.Identify() },
|
|
func(*server.Server) {},
|
|
),
|
|
)
|
|
|
|
startCtx, cancelStart := context.WithTimeout(
|
|
context.Background(), app.StartTimeout())
|
|
defer cancelStart()
|
|
|
|
err := app.Start(startCtx)
|
|
if err != nil {
|
|
// fx wraps a constructor's error in its own text; the cause
|
|
// alone names the setting. log is nil only if fx failed
|
|
// before it built the logger.
|
|
cause := dig.RootCause(err)
|
|
if log == nil {
|
|
_, _ = fmt.Fprintln(os.Stderr, cause)
|
|
} else {
|
|
log.Get().Error("cannot start", "error", cause)
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
// A signal, or the server asking to stop after a listen error.
|
|
shutdown := <-app.Wait()
|
|
|
|
stopCtx, cancelStop := context.WithTimeout(
|
|
context.Background(), app.StopTimeout())
|
|
defer cancelStop()
|
|
|
|
err = app.Stop(stopCtx)
|
|
if err != nil {
|
|
log.Get().Error("cannot stop cleanly", "error", err)
|
|
|
|
return 1
|
|
}
|
|
|
|
return shutdown.ExitCode
|
|
}
|