feat: deliver Sense audits to Bell (T-016)
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
|
||||
"yovision/bell/internal/audit"
|
||||
"yovision/bell/internal/store"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
type configuration struct {
|
||||
address string
|
||||
dsn string
|
||||
keyFile string
|
||||
tlsCert string
|
||||
tlsKey string
|
||||
}
|
||||
|
||||
func main() {
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
if err := run(logger); err != nil {
|
||||
logger.Error("Bell stopped", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfiguration() (configuration, error) {
|
||||
value := configuration{
|
||||
address: envOr("BELL_HTTP_ADDR", "127.0.0.1:8081"),
|
||||
dsn: os.Getenv("BELL_DB_DSN"),
|
||||
keyFile: os.Getenv("BELL_AUDIT_KEYS_FILE"),
|
||||
tlsCert: os.Getenv("BELL_TLS_CERT_FILE"),
|
||||
tlsKey: os.Getenv("BELL_TLS_KEY_FILE"),
|
||||
}
|
||||
if value.dsn == "" {
|
||||
return configuration{}, errors.New("BELL_DB_DSN is required")
|
||||
}
|
||||
if value.keyFile == "" || !filepath.IsAbs(value.keyFile) {
|
||||
return configuration{}, errors.New("BELL_AUDIT_KEYS_FILE must be an absolute external path")
|
||||
}
|
||||
host, _, err := net.SplitHostPort(value.address)
|
||||
if err != nil {
|
||||
return configuration{}, errors.New("invalid BELL_HTTP_ADDR")
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
loopback := host == "localhost" || (ip != nil && ip.IsLoopback())
|
||||
if !loopback && (value.tlsCert == "" || value.tlsKey == "" || !filepath.IsAbs(value.tlsCert) || !filepath.IsAbs(value.tlsKey)) {
|
||||
return configuration{}, errors.New("non-loopback Bell bind requires absolute TLS certificate and key paths")
|
||||
}
|
||||
if (value.tlsCert == "") != (value.tlsKey == "") {
|
||||
return configuration{}, errors.New("Bell TLS certificate and key must be configured together")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func run(logger *slog.Logger) error {
|
||||
cfg, err := loadConfiguration()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pgConfig, err := pgx.ParseConfig(cfg.dsn)
|
||||
if err != nil {
|
||||
return errors.New("invalid Bell postgres DSN")
|
||||
}
|
||||
if pgConfig.RuntimeParams == nil {
|
||||
pgConfig.RuntimeParams = make(map[string]string)
|
||||
}
|
||||
pgConfig.RuntimeParams["application_name"] = "yovision-bell"
|
||||
db := stdlib.OpenDB(*pgConfig)
|
||||
db.SetMaxOpenConns(16)
|
||||
db.SetMaxIdleConns(4)
|
||||
db.SetConnMaxLifetime(30 * time.Minute)
|
||||
defer db.Close()
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
repository, err := store.OpenPostgres(ctx, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := repository.AuditRelayReady(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
keys, err := audit.LoadKeys(cfg.keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handler, err := audit.NewHandler(repository, keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(audit.RelayPath, handler)
|
||||
mux.HandleFunc("GET /healthz", func(writer http.ResponseWriter, _ *http.Request) {
|
||||
writeStatus(writer, http.StatusOK, "ok")
|
||||
})
|
||||
mux.HandleFunc("GET /readyz", func(writer http.ResponseWriter, request *http.Request) {
|
||||
if err := repository.AuditRelayReady(request.Context()); err != nil {
|
||||
writeStatus(writer, http.StatusServiceUnavailable, "not_ready")
|
||||
return
|
||||
}
|
||||
writeStatus(writer, http.StatusOK, "ready")
|
||||
})
|
||||
server := &http.Server{Addr: cfg.address, Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}}
|
||||
serverErrors := make(chan error, 1)
|
||||
go func() {
|
||||
logger.Info("Bell listening", "address", cfg.address, "version", version, "tls_enabled", cfg.tlsCert != "")
|
||||
if cfg.tlsCert != "" {
|
||||
serverErrors <- server.ListenAndServeTLS(cfg.tlsCert, cfg.tlsKey)
|
||||
return
|
||||
}
|
||||
serverErrors <- server.ListenAndServe()
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case serverErr := <-serverErrors:
|
||||
if !errors.Is(serverErr, http.ErrServerClosed) {
|
||||
return fmt.Errorf("serve Bell HTTP: %w", serverErr)
|
||||
}
|
||||
}
|
||||
shutdownContext, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return server.Shutdown(shutdownContext)
|
||||
}
|
||||
|
||||
func writeStatus(writer http.ResponseWriter, status int, value string) {
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(status)
|
||||
_, _ = fmt.Fprintf(writer, `{"status":%q}`, value)
|
||||
}
|
||||
|
||||
func envOr(name, fallback string) string {
|
||||
if value := os.Getenv(name); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user