117 lines
2.9 KiB
Go
117 lines
2.9 KiB
Go
// 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 <comando>
|
|
|
|
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 <comando> tarefas administrativas (botpontos admin para ver a lista)
|
|
healthcheck <url> 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 <url>")
|
|
}
|
|
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
|
|
}
|