DoH через DoWithRetry; CDN preview через UpstreamHTTPDo; частичный fail CDN (EVOBGP_CDN_PARTIAL_OK); безопасный доступ к Job.Meta; drain jobs при SIGTERM; ValidateProductionEnforce при EVOBGP_PRODUCTION=1. Co-authored-by: Cursor <[email protected]>
149 lines
4.6 KiB
Go
149 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"evobgp/internal/birdfmt"
|
|
"evobgp/internal/config"
|
|
"evobgp/internal/dbcli"
|
|
"evobgp/internal/deploy"
|
|
"evobgp/internal/httpapi"
|
|
"evobgp/internal/ingest"
|
|
"evobgp/internal/observability"
|
|
"evobgp/internal/platform"
|
|
"evobgp/internal/render"
|
|
"evobgp/internal/scheduler"
|
|
"evobgp/internal/version"
|
|
)
|
|
|
|
// microVPS entrypoint: один процесс — HTTP API и фоновые воркеры scheduler, ingest, render, deploy (общий store и jobs.Registry).
|
|
func main() {
|
|
if len(os.Args) > 1 && os.Args[1] == "db" {
|
|
os.Exit(dbcli.Run(os.Args[2:]))
|
|
}
|
|
cfg := config.Load()
|
|
if err := config.ValidateProductionEnforce(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
opts := httpapi.Options{
|
|
APIKeys: os.Getenv("EVOBGP_API_KEYS"),
|
|
DatabaseURL: cfg.DatabaseURL,
|
|
InsecureDev: os.Getenv("EVOBGP_DEV_INSECURE") == "1",
|
|
SeedDemo: os.Getenv("EVOBGP_SEED_DEMO") != "0",
|
|
BundleSeedHex: strings.TrimSpace(os.Getenv("EVOBGP_BUNDLE_SEED_HEX")),
|
|
CORSAllowedOrigins: strings.TrimSpace(os.Getenv("EVOBGP_CORS_ORIGINS")),
|
|
RuntimeLogsPolicyTenant: cfg.RuntimeLogsPolicyTenant,
|
|
JWTSecret: firstNonEmpty(os.Getenv("EVOBGP_AUTH_JWT_SECRET"), os.Getenv("AUTH_JWT_SECRET")),
|
|
AuthIssuer: firstNonEmpty(os.Getenv("EVOBGP_AUTH_ISSUER"), os.Getenv("AUTH_ISSUER")),
|
|
AuthPortalURL: firstNonEmpty(os.Getenv("EVOBGP_AUTH_PORTAL_URL"), os.Getenv("AUTH_PORTAL_URL")),
|
|
PortalTenantID: strings.TrimSpace(os.Getenv("EVOBGP_PORTAL_TENANT_ID")),
|
|
AuthRequired: boolFromEnv("EVOBGP_AUTH_REQUIRED", "AUTH_REQUIRED"),
|
|
AuditIngestSecret: firstNonEmpty(os.Getenv("EVOBGP_AUTH_AUDIT_INGEST_SECRET"), os.Getenv("AUTH_AUDIT_INGEST_SECRET")),
|
|
}
|
|
srv, err := httpapi.New(opts)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer srv.Close()
|
|
observability.SetBuildInfo(version.Version, version.GitSHA)
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
schedDeps := &scheduler.Deps{Store: srv.Store(), Jobs: srv.Jobs()}
|
|
ingestDeps := &ingest.Deps{Store: srv.Store()}
|
|
renderDeps := &render.Deps{Store: srv.Store()}
|
|
deployDeps := &deploy.Deps{Store: srv.Store()}
|
|
go scheduler.Run(ctx, schedDeps)
|
|
go ingest.Run(ctx, ingestDeps)
|
|
go render.Run(ctx, renderDeps)
|
|
go deploy.Run(ctx, deployDeps)
|
|
|
|
srv.StartBackground(ctx)
|
|
startBirdMetricsPoller(ctx)
|
|
|
|
httpSrv := &http.Server{
|
|
Addr: cfg.HTTPAddr,
|
|
Handler: srv.Handler(),
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
ReadTimeout: 60 * time.Second,
|
|
WriteTimeout: 120 * time.Second,
|
|
IdleTimeout: 120 * time.Second,
|
|
}
|
|
go func() {
|
|
svc := platform.ServiceName("evobgp-all")
|
|
log.Printf("%s (microVPS): HTTP API on %s", svc, cfg.HTTPAddr)
|
|
log.Printf("bundle public key (base64): %s", srv.BundlePublicKeyBase64())
|
|
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatal(err)
|
|
}
|
|
}()
|
|
|
|
<-ctx.Done()
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
n := srv.Jobs().RequestCancelAll()
|
|
if n > 0 {
|
|
log.Printf("draining %d job(s)…", n)
|
|
if err := srv.Jobs().Drain(shutdownCtx); err != nil {
|
|
log.Printf("job drain: %v", err)
|
|
}
|
|
}
|
|
if err := httpSrv.Shutdown(shutdownCtx); err != nil {
|
|
log.Printf("HTTP shutdown: %v", err)
|
|
}
|
|
log.Printf("%s stopped", platform.ServiceName("evobgp-all"))
|
|
}
|
|
|
|
func firstNonEmpty(candidates ...string) string {
|
|
for _, c := range candidates {
|
|
if v := strings.TrimSpace(c); v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func boolFromEnv(keys ...string) bool {
|
|
for _, k := range keys {
|
|
v := strings.TrimSpace(os.Getenv(k))
|
|
if v == "" {
|
|
continue
|
|
}
|
|
switch strings.ToLower(v) {
|
|
case "1", "true", "yes", "on":
|
|
return true
|
|
case "0", "false", "no", "off":
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func startBirdMetricsPoller(ctx context.Context) {
|
|
sock := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_SOCKET"))
|
|
if sock == "" {
|
|
return
|
|
}
|
|
interval := 30 * time.Second
|
|
if d, err := time.ParseDuration(strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_INTERVAL"))); err == nil && d > 0 {
|
|
interval = d
|
|
}
|
|
bin := strings.TrimSpace(os.Getenv("EVOBGP_BIRDC_BIN"))
|
|
observability.StartBirdProtocolsPoller(ctx, sock, bin, interval,
|
|
func(ctx context.Context, socket, birdcBin string) (string, error) {
|
|
return birdfmt.ShowProtocols(ctx, socket, birdcBin)
|
|
},
|
|
birdfmt.CountEstablishedBGPSessions,
|
|
birdfmt.ParseBGPProtocolStates,
|
|
)
|
|
log.Printf("birdc protocols poller enabled (socket=%s interval=%s)", sock, interval)
|
|
}
|