// Command snip is a single binary exposing three subcommands — api, frontend // and redirect — so one Docker image can back three docker-compose services. package main import ( "context" "errors" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/snip/backend/internal/adapter/postgres" "github.com/snip/backend/internal/config" "github.com/snip/backend/internal/di" ) func main() { log.SetFlags(log.LstdFlags | log.Lmsgprefix) if len(os.Args) < 2 { usage() os.Exit(2) } cmd := os.Args[1] cfg := config.Load() ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() container := di.New(cfg) var ( app *di.App addr string err error ) switch cmd { case "migrate": if cfg.Store != "postgres" { log.Printf("snip migrate: STORE=%s, nothing to do", cfg.Store) os.Exit(0) } if err := postgres.Migrate(ctx, cfg.DatabaseURL); err != nil { log.Fatalf("snip migrate: %v", err) } log.Println("snip migrate: schema is up to date") os.Exit(0) case "frontend": app, addr = container.BuildFrontend(), listenAddr("8081") case "api": app, err = container.BuildAPI(ctx) addr = listenAddr("8080") case "redirect": app, err = container.BuildRedirect(ctx) addr = listenAddr("8082") default: usage() os.Exit(2) } if err != nil { log.Fatalf("snip %s: startup failed: %v", cmd, err) } defer app.Close() serve(ctx, cmd, addr, app.Handler) } func listenAddr(def string) string { if p := os.Getenv("PORT"); p != "" { return ":" + p } return ":" + def } func serve(ctx context.Context, name, addr string, h http.Handler) { srv := &http.Server{ Addr: addr, Handler: h, ReadHeaderTimeout: 5 * time.Second, } go func() { log.Printf("snip %s listening on %s", name, addr) if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("snip %s: %v", name, err) } }() <-ctx.Done() log.Printf("snip %s: shutting down", name) shutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _ = srv.Shutdown(shutCtx) } func usage() { fmt.Fprintln(os.Stderr, "usage: snip ") }