// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini package main import ( "context" "database/sql" "errors" "fmt" "io" "log/slog" "net" "os" "os/signal" "strings" "syscall" "time" ) // Exit codes const ( exitOK = 0 exitFailure = 1 exitUsage = 2 ) // errUsage marks command-line usage errors (exit code 2, usage text printed) var errUsage = errors.New("usage error") // usageText is printed to stderr after a usage error and to stdout by `help` const usageText = `usage: dpsapi start the HTTP server dpsapi help print this usage text dpsapi token list [all] list the active tokens, or every token with "all" dpsapi token new [description] create a token and print it dpsapi token delete deactivate a token dpsapi audit print the latest 10 audit rows dpsapi audit print the latest n audit rows dpsapi audit print the audit rows of the last n units (s, m, h, d, w) dpsapi audit all print every audit row ` // usageErrorf builds an error wrapping errUsage func usageErrorf(format string, args ...any) error { return fmt.Errorf("%w: "+format, append([]any{errUsage}, args...)...) } // dockerConn is a closable container lister type dockerConn interface { ContainerLister Close() error } // app holds the process-level dependencies; every field is replaceable in tests type app struct { lookupEnv func(string) (string, bool) stdout io.Writer stderr io.Writer now func() time.Time generateToken func() (string, error) newDocker func(log *slog.Logger) (dockerConn, error) listen func(network, address string) (net.Listener, error) shutdownTimeout time.Duration } // newApp returns an app wired with the production dependencies func newApp(lookupEnv func(string) (string, bool), stdout, stderr io.Writer) *app { return &app{ lookupEnv: lookupEnv, stdout: stdout, stderr: stderr, now: time.Now, generateToken: GenerateToken, newDocker: func(log *slog.Logger) (dockerConn, error) { return NewDockerClient(dockerHost, log) }, listen: net.Listen, shutdownTimeout: defaultShutdownTimeout, } } func main() { os.Exit(run(os.Args[1:], os.LookupEnv, os.Stdout, os.Stderr)) } // run is the process entry point: it wires SIGINT/SIGTERM to cancellation and returns the exit code func run(args []string, lookupEnv func(string) (string, bool), stdout, stderr io.Writer) int { // Create a context cancelled by the OS signals ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() return newApp(lookupEnv, stdout, stderr).run(ctx, args) } // run executes the command and maps its outcome to the exit code // The "dpsapi: ..." error line is therefore always the last line written to stderr func (a *app) run(ctx context.Context, args []string) int { return a.exitCode(a.execute(ctx, args)) } // execute loads the configuration, initializes the logger and dispatches on the arguments func (a *app) execute(ctx context.Context, args []string) (err error) { // Load and validate the configuration before anything else cfg, err := LoadConfig(a.lookupEnv) if err != nil { return err } // Initialize the logger log := NewLogger(a.stderr, cfg.LogLevel) defer traceFunc(ctx, log, "app.execute")() log.DebugContext(ctx, "configuration resolved", "db_path", cfg.DBPath, "http_addr", cfg.HTTPAddr, "http_port", cfg.HTTPPort, "log_level", LevelName(cfg.LogLevel)) // Dispatch switch { case len(args) == 0: log.DebugContext(ctx, "dispatch", "mode", "server") err = a.runServer(ctx, cfg, log) case args[0] == "help": log.DebugContext(ctx, "dispatch", "mode", "cli") err = a.runHelpCommand(ctx, log, args[1:]) case args[0] == "token": log.DebugContext(ctx, "dispatch", "mode", "cli") err = a.runTokenCommand(ctx, cfg, log, args[1:]) case args[0] == "audit": log.DebugContext(ctx, "dispatch", "mode", "cli") err = a.runAuditCommand(ctx, cfg, log, args[1:]) default: err = usageErrorf("unknown command") } return err } // runHelpCommand prints the usage text to stdout; the database is never opened func (a *app) runHelpCommand(ctx context.Context, log *slog.Logger, args []string) error { defer traceFunc(ctx, log, "app.runHelpCommand")() if len(args) != 0 { return usageErrorf("help takes no arguments") } log.DebugContext(ctx, "cli dispatch", "command", "help") if _, err := fmt.Fprint(a.stdout, usageText); err != nil { return fmt.Errorf("write output: %w", err) } return nil } // exitCode prints err (if any) to stderr as a single line and maps it to the process exit code func (a *app) exitCode(err error) int { if err == nil { return exitOK } msg := strings.NewReplacer("\r", " ", "\n", " ").Replace(err.Error()) fmt.Fprintf(a.stderr, "dpsapi: %s\n", msg) switch { case errors.Is(err, errUsage): fmt.Fprint(a.stderr, usageText) return exitUsage case errors.Is(err, errConfig): return exitUsage default: return exitFailure } } // closeDatabase closes the pool, keeping the first error func closeDatabase(ctx context.Context, db *sql.DB, log *slog.Logger, err *error) { if cerr := db.Close(); cerr != nil && *err == nil { *err = fmt.Errorf("close database: %w", cerr) } log.DebugContext(ctx, "database closed") } // runServer runs the HTTP server until ctx is cancelled func (a *app) runServer(ctx context.Context, cfg Config, log *slog.Logger) (err error) { defer traceFunc(ctx, log, "app.runServer")() // Open the database db, err := OpenDatabase(ctx, cfg.DBPath, log) if err != nil { return err } defer closeDatabase(ctx, db, log, &err) tokens := NewTokenStore(db, log, a.now, a.generateToken) audit := NewAuditStore(db, log, a.now) // Create the Docker client (lazy: no connection is made yet) docker, err := a.newDocker(log) if err != nil { return err } defer docker.Close() log.DebugContext(ctx, "docker client created", "host", dockerHost) // Create the web server and register the routes ws := NewWebServer(log, tokens, audit, docker) // Listen in the foreground ln, err := a.listen(cfg.ListenNetwork(), cfg.ListenAddress()) if err != nil { return err } if err := ws.Serve(ctx, ln, a.shutdownTimeout); err != nil { return err } log.InfoContext(ctx, "server stopped") return nil }