// 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 ( "fmt" "io" "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" ) // 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. 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. When serve is RunServer and the server fails to // start, to listen or to stop, fx's Run exits the process with status 1 // and Main never returns. func Main( stderr io.Writer, 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(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 } 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, to listen 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() }