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, and logs fx's "received signal" line as Run did. 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
195 lines
5.7 KiB
Go
195 lines
5.7 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
|
|
fxLog *fxevent.SlogLogger
|
|
)
|
|
|
|
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.
|
|
// fx's Run logs the signal here, so this does too.
|
|
shutdown := <-app.Wait()
|
|
fxLog.LogEvent(&fxevent.Stopping{Signal: shutdown.Signal})
|
|
|
|
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
|
|
}
|