package cmd import ( "context" "errors" "fmt" "net/http" "os" "os/signal" "strconv" "strings" "syscall" "time" "git.ilapage.cn/OPC/chorus/admin/app/admin/router" "git.ilapage.cn/OPC/chorus/admin/app/chorus" "git.ilapage.cn/OPC/chorus/admin/common/database" common "git.ilapage.cn/OPC/chorus/admin/common/middleware" ext "git.ilapage.cn/OPC/chorus/admin/config" safehttp "git.ilapage.cn/OPC/chorus/internal/platform/http" "github.com/gin-gonic/gin" "github.com/go-admin-team/go-admin-core/config/source/file" "github.com/go-admin-team/go-admin-core/sdk" coreapi "github.com/go-admin-team/go-admin-core/sdk/api" coreconfig "github.com/go-admin-team/go-admin-core/sdk/config" "github.com/go-admin-team/go-admin-core/sdk/pkg" "github.com/spf13/cobra" "gorm.io/gorm" ) var configPath string var rootCmd = &cobra.Command{ Use: "chorus-admin", SilenceUsage: true, } var serverCmd = &cobra.Command{ Use: "server", Short: "Start the Chorus go-admin backend", PreRunE: func(cmd *cobra.Command, args []string) error { return setup(configPath) }, RunE: func(cmd *cobra.Command, args []string) error { return serve() }, } func init() { serverCmd.Flags().StringVarP(&configPath, "config", "c", "", "go-admin settings file") rootCmd.AddCommand(serverCmd) } func Execute() error { return rootCmd.Execute() } func setup(path string) error { if strings.TrimSpace(path) == "" { return errors.New("--config is required; use config/settings.example.yml as a starting point") } coreconfig.ExtendConfig = &ext.ExtConfig coreconfig.Setup(file.NewSource(file.WithPath(path)), database.Setup) serviceConfig, err := serviceConfigFromEnvironment(os.LookupEnv) if err != nil { return err } chorus.Configure(func(db *gorm.DB) (*chorus.Service, error) { return chorus.NewService(db, serviceConfig.config) }) return nil } type configuredService struct { config chorus.Config } func serviceConfigFromEnvironment(lookup func(string) (string, bool)) (configuredService, error) { read := func(name string) string { value, _ := lookup(name) return strings.TrimSpace(value) } configured := configuredService{} allowRaw := read("CHORUS_ADMIN_ALLOW_CONNECTIVITY_PROBES") if allowRaw == "" || allowRaw == "0" { return configured, nil } if allowRaw != "1" { return configuredService{}, errors.New("CHORUS_ADMIN_ALLOW_CONNECTIVITY_PROBES must be 1 when set") } cooldown, err := positiveDuration(read("CHORUS_ADMIN_CONNECTIVITY_COOLDOWN_SECONDS"), time.Second) if err != nil { return configuredService{}, errors.New("CHORUS_ADMIN_CONNECTIVITY_COOLDOWN_SECONDS must be a positive integer") } timeout, err := positiveDuration(read("CHORUS_PROVIDER_HTTP_TIMEOUT_SECONDS"), time.Second) if err != nil { return configuredService{}, errors.New("CHORUS_PROVIDER_HTTP_TIMEOUT_SECONDS must be a positive integer") } maxResponse, err := positiveInt64(read("CHORUS_PROVIDER_MAX_RESPONSE_BYTES")) if err != nil { return configuredService{}, errors.New("CHORUS_PROVIDER_MAX_RESPONSE_BYTES must be a positive integer") } client, err := safehttp.New(safehttp.Config{Timeout: timeout, MaxRedirects: 3}) if err != nil { return configuredService{}, errors.New("configure SSRF-safe connectivity client") } probe, err := chorus.NewLiveProbe(safehttp.NewProviderClient(client)) if err != nil { return configuredService{}, errors.New("configure connectivity probe") } configured.config = chorus.Config{AllowConnectivityChecks: true, ConnectivityCooldown: cooldown, MaxResponseBytes: maxResponse, Probe: probe} return configured, nil } func positiveDuration(raw string, unit time.Duration) (time.Duration, error) { value, err := strconv.ParseInt(raw, 10, 64) if err != nil || value <= 0 { return 0, errors.New("invalid positive duration") } return time.Duration(value) * unit, nil } func positiveInt64(raw string) (int64, error) { value, err := strconv.ParseInt(raw, 10, 64) if err != nil || value <= 0 { return 0, errors.New("invalid positive integer") } return value, nil } func serve() error { if coreconfig.ApplicationConfig.Mode == pkg.ModeProd.String() { gin.SetMode(gin.ReleaseMode) } engine := gin.New() sdk.Runtime.SetEngine(engine) engine.Use(common.Sentinel()).Use(common.RequestId(pkg.TrafficKey)).Use(coreapi.SetRequestLogger) common.InitMiddleware(engine) router.InitRouter() server := &http.Server{ Addr: fmt.Sprintf("%s:%d", coreconfig.ApplicationConfig.Host, coreconfig.ApplicationConfig.Port), Handler: engine, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: time.Duration(coreconfig.ApplicationConfig.ReadTimeout) * time.Second, WriteTimeout: time.Duration(coreconfig.ApplicationConfig.WriterTimeout) * time.Second, IdleTimeout: 60 * time.Second, } shutdown, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() result := make(chan error, 1) go func() { result <- server.ListenAndServe() }() select { case <-shutdown.Done(): ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() return server.Shutdown(ctx) case err := <-result: if errors.Is(err, http.ErrServerClosed) { return nil } return err } }