187 lines
5.6 KiB
Go
187 lines
5.6 KiB
Go
// Package main is the entry point for the webhooker application.
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"time"
|
|
|
|
"go.uber.org/fx"
|
|
"sneak.berlin/go/webhooker/internal/config"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/datadir"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
"sneak.berlin/go/webhooker/internal/globals"
|
|
"sneak.berlin/go/webhooker/internal/handlers"
|
|
"sneak.berlin/go/webhooker/internal/healthcheck"
|
|
"sneak.berlin/go/webhooker/internal/logger"
|
|
"sneak.berlin/go/webhooker/internal/middleware"
|
|
"sneak.berlin/go/webhooker/internal/resetpw"
|
|
"sneak.berlin/go/webhooker/internal/server"
|
|
"sneak.berlin/go/webhooker/internal/session"
|
|
)
|
|
|
|
// stopTimeout bounds the whole fx stop sequence, not each hook.
|
|
//
|
|
// fx defaults to 15s, which is longer than Docker's 10s default
|
|
// stop grace: the container would be SIGKILLed before the bound
|
|
// could fire, so nothing bounded by it would ever be observed.
|
|
// 5s leaves headroom inside that grace for signal delivery and
|
|
// process exit; the observed wedge case already exits at ~5.3s,
|
|
// so a larger bound would trade a rare skipped database close for
|
|
// a more common hard kill.
|
|
//
|
|
// The server's stop hook must fit inside it with room to spare: a
|
|
// hook that used the whole budget would exhaust it at that instant,
|
|
// and fx would skip every hook after the server — the delivery
|
|
// engine, the healthcheck, the webhook DB manager and the database
|
|
// close. That hook is the 3s HTTP drain plus the Sentry flush that
|
|
// follows it in the same hook, so the flush is clamped to the stop
|
|
// context's remaining time less server.TailHookReserve rather than
|
|
// running for its own fixed 2s; the reserve is what the tail hooks
|
|
// live on, and they are microsecond-scale in normal operation.
|
|
// TestStopTimeout_LeavesHeadroomForTailHooks pins the arithmetic
|
|
// across every drain length.
|
|
//
|
|
// This does not make the database close unconditional: the
|
|
// ArchiveSweeper and RetentionReaper hooks run before the server
|
|
// and can still consume the whole budget on their own.
|
|
const stopTimeout = 5 * time.Second
|
|
|
|
// exitUsage is the status for a command line this binary cannot make
|
|
// sense of, kept distinct from the 1 a refusal exits with so that a
|
|
// caller can tell "called wrong" from "declined".
|
|
const exitUsage = 2
|
|
|
|
// Build-time variables set via -ldflags.
|
|
//
|
|
//nolint:gochecknoglobals // Build-time variables injected by the linker.
|
|
var (
|
|
version = "dev"
|
|
appname = "webhooker"
|
|
)
|
|
|
|
func main() {
|
|
globals.Appname = appname
|
|
globals.Version = version
|
|
|
|
os.Exit(dispatch(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
|
|
}
|
|
|
|
// dispatch routes the command line to a subcommand.
|
|
//
|
|
// No arguments runs the server, which is what the image's CMD and
|
|
// every existing deployment invoke; that path is unchanged, including
|
|
// where the DATA_DIR lock is taken relative to building the fx graph
|
|
// and how fx propagates a non-zero exit itself.
|
|
func dispatch(
|
|
args []string,
|
|
stdin io.Reader,
|
|
stdout, stderr io.Writer,
|
|
) int {
|
|
if len(args) == 0 {
|
|
return run(stderr)
|
|
}
|
|
|
|
switch args[0] {
|
|
case resetpw.Name:
|
|
return resetpw.Run(args[1:], stdin, stdout, stderr)
|
|
case "help", "-h", "-help", "--help":
|
|
usage(stdout)
|
|
|
|
return 0
|
|
default:
|
|
_, _ = fmt.Fprintf(
|
|
stderr, "%s: unknown subcommand %q\n", appname, args[0],
|
|
)
|
|
usage(stderr)
|
|
|
|
return exitUsage
|
|
}
|
|
}
|
|
|
|
// usage lists what the binary can be asked to do.
|
|
func usage(w io.Writer) {
|
|
_, _ = fmt.Fprintf(w, `usage: %s [subcommand]
|
|
|
|
With no subcommand, runs the webhooker server.
|
|
|
|
Subcommands:
|
|
%s [-generate] <username>
|
|
Set an existing account's password on a stopped deployment.
|
|
Recovers an admin account whose bootstrap password was lost.
|
|
help
|
|
Print this message.
|
|
`, appname, resetpw.Name)
|
|
}
|
|
|
|
// run takes the exclusive DATA_DIR lock, then runs the application
|
|
// under it, and returns the process exit status.
|
|
//
|
|
// The lock is taken here rather than inside the fx graph because it has
|
|
// to be held before anything opens a database, and because a refusal
|
|
// has to reach the operator as a plain line on standard error rather
|
|
// than as one entry in an fx failure dump. It is released by the defer
|
|
// on a clean shutdown, and by the kernel closing the descriptor on any
|
|
// other exit — including the one fx performs itself when a start or
|
|
// stop hook fails, which skips deferred calls.
|
|
func run(stderr io.Writer) int {
|
|
lock, err := datadir.Acquire(config.DataDir())
|
|
if err != nil {
|
|
_, _ = fmt.Fprintf(stderr, "%s: %v\n", appname, err)
|
|
|
|
return 1
|
|
}
|
|
|
|
defer func() { _ = lock.Release() }()
|
|
|
|
newApp().Run()
|
|
|
|
return 0
|
|
}
|
|
|
|
// newApp builds the application graph. It is separate from main so
|
|
// a test can assert the options it carries.
|
|
func newApp() *fx.App {
|
|
return fx.New(
|
|
fx.StopTimeout(stopTimeout),
|
|
fx.Provide(
|
|
globals.New,
|
|
logger.New,
|
|
config.New,
|
|
database.New,
|
|
database.NewWebhookDBManager,
|
|
database.NewRetentionReaper,
|
|
healthcheck.New,
|
|
session.New,
|
|
handlers.New,
|
|
middleware.New,
|
|
// The one SSRF guard both target-creation validation
|
|
// and the delivery dialer consult, so they cannot
|
|
// disagree about a destination.
|
|
delivery.NewGuard,
|
|
delivery.New,
|
|
delivery.NewArchiveSweeper,
|
|
// Wire *delivery.Engine as delivery.Notifier so the
|
|
// webhook handler can notify the engine of new deliveries.
|
|
func(e *delivery.Engine) delivery.Notifier { return e },
|
|
// Wire *delivery.Engine as delivery.WebhookEvictor so
|
|
// deleting a webhook releases its archive writer.
|
|
func(e *delivery.Engine) delivery.WebhookEvictor {
|
|
return e
|
|
},
|
|
server.New,
|
|
),
|
|
fx.Invoke(
|
|
func(
|
|
*server.Server,
|
|
*delivery.Engine,
|
|
*database.RetentionReaper,
|
|
*delivery.ArchiveSweeper,
|
|
) {
|
|
},
|
|
),
|
|
)
|
|
}
|