package main import ( "context" "errors" "log" "net/http" "os" "os/signal" "strings" "syscall" "time" "github.com/jackc/pgx/v5/pgxpool" "knowledge-governance/internal/appconfig" "knowledge-governance/internal/httpapi" "knowledge-governance/internal/monitoring" postgresstore "knowledge-governance/internal/platform/postgres" "knowledge-governance/internal/platform/weknora" ) func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() if err := run(ctx, os.Getenv); err != nil { log.Fatal(err) } } func run(ctx context.Context, getenv func(string) string) error { config, err := appconfig.Load(getenv) if err != nil { return err } pool, err := pgxpool.New(ctx, config.DatabaseURL) if err != nil { return err } defer pool.Close() if err := pool.Ping(ctx); err != nil { return err } if err := postgresstore.ApplyMigrations(ctx, pool); err != nil { return err } releaseAuthorizationToken := "" if config.ProductionReleaseRequestsEnabled { releaseAuthorizationToken, err = weknora.ReadAPIKeyFile(config.ProductionReleaseAuthTokenFile) if err != nil { return errors.New("read production release authorization token") } } // P10: with a mounted API key the probe verifies a real knowledge-base read // (readiness); without one it degrades to the liveness-only HTTP check. var weknoraReadiness func(context.Context) error if strings.TrimSpace(config.TestWeKnoraBaseURL) != "" { if apiKey, keyErr := weknora.ReadAPIKeyFile(config.TestWeKnoraAPIKeyFile); keyErr == nil { probeTimeout := config.TestWeKnoraHTTPTimeout if probeTimeout > 10*time.Second || probeTimeout <= 0 { probeTimeout = 10 * time.Second } if probeClient, clientErr := weknora.NewClient(weknora.ClientConfig{BaseURL: config.TestWeKnoraBaseURL, APIKey: apiKey, Timeout: probeTimeout, AllowDockerHTTP: config.TestWeKnoraAllowDockerHTTP}); clientErr == nil { routingKB := config.InternalRoutingKnowledgeBaseID weknoraReadiness = func(probeCtx context.Context) error { _, _, err := probeClient.ListKnowledge(probeCtx, routingKB, 1, 1) return err } } } } monitorProvider, err := monitoring.NewRealProvider(monitoring.RealProviderConfig{DB: pool, WeKnoraBaseURL: config.TestWeKnoraBaseURL, MinerUBaseURL: config.MinerUBaseURL, WeKnoraReadiness: weknoraReadiness, WorkerStaleAfter: config.WorkerLivenessStaleAfter}) if err != nil { return err } handler, err := httpapi.NewHandler(httpapi.Config{ Store: postgresstore.NewStore(pool), Monitor: monitorProvider, InternalRoutingKnowledgeBaseID: config.InternalRoutingKnowledgeBaseID, ExternalRoutingKnowledgeBaseID: config.ExternalRoutingKnowledgeBaseID, ProductionReleaseRequestsEnabled: config.ProductionReleaseRequestsEnabled, ProductionBackupRestoreVerified: config.ProductionBackupRestoreVerified, ProductionReleaseAuthorizerRef: config.ProductionReleaseAuthorizerRef, ProductionReleaseAuthorizationToken: releaseAuthorizationToken, }) if err != nil { return err } server := &http.Server{ Addr: config.AppAddr, Handler: handler, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, } errCh := make(chan error, 1) go func() { errCh <- server.ListenAndServe() }() log.Printf("governance API listening on %s", config.AppAddr) select { case err := <-errCh: if errors.Is(err, http.ErrServerClosed) { return nil } return err case <-ctx.Done(): shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return server.Shutdown(shutdownCtx) } }