// Comando botpontos: um único binário com os papéis api, worker, migrate e admin. package main import ( "context" "errors" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" _ "time/tzdata" // a imagem distroless não tem /usr/share/zoneinfo "botpontos/internal/adapters/postgres" "botpontos/internal/config" "botpontos/internal/httpapi" "botpontos/internal/worker" ) // version é preenchida no build com -ldflags "-X main.version=...". var version = "dev" const usage = `uso: botpontos comandos: api inicia a API REST worker inicia o worker (bot do Telegram e jobs) migrate [up|status|down] aplica ou consulta as migrations (padrão: up) admin tarefas administrativas (botpontos admin para ver a lista) healthcheck verifica um endpoint de saúde (usado pelo Docker) version mostra a versão ` func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() if err := run(ctx, os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, "erro:", err) os.Exit(1) } } func run(ctx context.Context, args []string) error { if len(args) == 0 { fmt.Fprint(os.Stderr, usage) return errors.New("comando não informado") } switch args[0] { case "version": fmt.Println(version) return nil case "healthcheck": if len(args) != 2 { return errors.New("uso: botpontos healthcheck ") } return healthcheck(ctx, args[1]) case "api", "worker", "migrate", "admin": // precisam de configuração; seguem abaixo default: fmt.Fprint(os.Stderr, usage) return fmt.Errorf("comando desconhecido: %s", args[0]) } cfg, err := config.Load() if err != nil { return fmt.Errorf("configuração inválida:\n%w", err) } logger := newLogger(cfg.LogLevel).With("component", args[0], "version", version) slog.SetDefault(logger) switch args[0] { case "api": return httpapi.Run(ctx, cfg, logger) case "worker": return worker.Run(ctx, cfg, logger) case "admin": return runAdmin(ctx, cfg, logger, args[1:]) default: // migrate command := "up" if len(args) > 1 { command = args[1] } return postgres.Migrate(ctx, cfg.DB, command, logger) } } func newLogger(level string) *slog.Logger { var lvl slog.Level _ = lvl.UnmarshalText([]byte(level)) // já validado em config.Load return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: lvl})) } // healthcheck existe porque a imagem distroless não tem curl nem wget. func healthcheck(ctx context.Context, url string) error { ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode > 299 { return fmt.Errorf("status %d", resp.StatusCode) } return nil }