1package serve23import (4 "context"5 "crypto/tls"6 "errors"7 "fmt"8 "net/http"910 "charm.land/log/v2"1112 "github.com/charmbracelet/soft-serve/pkg/backend"13 "github.com/charmbracelet/soft-serve/pkg/config"14 "github.com/charmbracelet/soft-serve/pkg/cron"15 "github.com/charmbracelet/soft-serve/pkg/daemon"16 "github.com/charmbracelet/soft-serve/pkg/db"17 "github.com/charmbracelet/soft-serve/pkg/jobs"18 sshsrv "github.com/charmbracelet/soft-serve/pkg/ssh"19 "github.com/charmbracelet/soft-serve/pkg/stats"20 "github.com/charmbracelet/soft-serve/pkg/web"21 "github.com/charmbracelet/ssh"22 "golang.org/x/sync/errgroup"23)2425// Server is the Soft Serve server.26type Server struct {27 SSHServer *sshsrv.SSHServer28 GitDaemon *daemon.GitDaemon29 HTTPServer *web.HTTPServer30 StatsServer *stats.StatsServer31 CertLoader *CertReloader32 Cron *cron.Scheduler33 Config *config.Config34 Backend *backend.Backend35 DB *db.DB3637 logger *log.Logger38 ctx context.Context39}4041// NewServer returns a new *Server configured to serve Soft Serve. The SSH42// server key-pair will be created if none exists.43// It expects a context with *backend.Backend, *db.DB, *log.Logger, and44// *config.Config attached.45func NewServer(ctx context.Context) (*Server, error) {46 var err error47 cfg := config.FromContext(ctx)48 be := backend.FromContext(ctx)49 db := db.FromContext(ctx)50 logger := log.FromContext(ctx).WithPrefix("server")51 srv := &Server{52 Config: cfg,53 Backend: be,54 DB: db,55 logger: log.FromContext(ctx).WithPrefix("server"),56 ctx: ctx,57 }5859 // Add cron jobs.60 sched := cron.NewScheduler(ctx)61 for n, j := range jobs.List() {62 spec := j.Runner.Spec(ctx)63 if spec == "" {64 continue65 }66 id, err := sched.AddFunc(spec, j.Runner.Func(ctx))67 if err != nil {68 logger.Warn("error adding cron job", "job", n, "err", err)69 }7071 j.ID = id72 }7374 srv.Cron = sched7576 srv.SSHServer, err = sshsrv.NewSSHServer(ctx)77 if err != nil {78 return nil, fmt.Errorf("create ssh server: %w", err)79 }8081 srv.GitDaemon, err = daemon.NewGitDaemon(ctx)82 if err != nil {83 return nil, fmt.Errorf("create git daemon: %w", err)84 }8586 srv.HTTPServer, err = web.NewHTTPServer(ctx)87 if err != nil {88 return nil, fmt.Errorf("create http server: %w", err)89 }9091 srv.StatsServer, err = stats.NewStatsServer(ctx)92 if err != nil {93 return nil, fmt.Errorf("create stats server: %w", err)94 }9596 if cfg.HTTP.TLSKeyPath != "" && cfg.HTTP.TLSCertPath != "" {97 srv.CertLoader, err = NewCertReloader(cfg.HTTP.TLSCertPath, cfg.HTTP.TLSKeyPath, logger)98 if err != nil {99 return nil, fmt.Errorf("create cert reloader: %w", err)100 }101102 srv.HTTPServer.SetTLSConfig(&tls.Config{103 GetCertificate: srv.CertLoader.GetCertificateFunc(),104 })105 }106107 return srv, nil108}109110// ReloadCertificates reloads the TLS certificates for the HTTP server.111func (s *Server) ReloadCertificates() error {112 if s.CertLoader == nil {113 return nil114 }115 return s.CertLoader.Reload()116}117118// Start starts the SSH server.119func (s *Server) Start() error {120 errg, _ := errgroup.WithContext(s.ctx)121122 // optionally start the SSH server123 if s.Config.SSH.Enabled {124 errg.Go(func() error {125 s.logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)126 if err := s.SSHServer.ListenAndServe(); !errors.Is(err, ssh.ErrServerClosed) {127 return err128 }129 return nil130 })131 }132133 // optionally start the git daemon134 if s.Config.Git.Enabled {135 errg.Go(func() error {136 s.logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)137 if err := s.GitDaemon.ListenAndServe(); !errors.Is(err, daemon.ErrServerClosed) {138 return err139 }140 return nil141 })142 }143144 // optionally start the HTTP server145 if s.Config.HTTP.Enabled {146 errg.Go(func() error {147 s.logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)148 if err := s.HTTPServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {149 return err150 }151 return nil152 })153 }154155 // optionally start the Stats server156 if s.Config.Stats.Enabled {157 errg.Go(func() error {158 s.logger.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)159 if err := s.StatsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {160 return err161 }162 return nil163 })164 }165166 errg.Go(func() error {167 s.Cron.Start()168 return nil169 })170 return errg.Wait()171}172173// Shutdown lets the server gracefully shutdown.174func (s *Server) Shutdown(ctx context.Context) error {175 errg, ctx := errgroup.WithContext(ctx)176 errg.Go(func() error {177 return s.GitDaemon.Shutdown(ctx)178 })179 errg.Go(func() error {180 return s.HTTPServer.Shutdown(ctx)181 })182 errg.Go(func() error {183 return s.SSHServer.Shutdown(ctx)184 })185 errg.Go(func() error {186 return s.StatsServer.Shutdown(ctx)187 })188 errg.Go(func() error {189 for _, j := range jobs.List() {190 // jobID from github.com/robfig/cron/v2 starts from 1191 if j.ID != 0 {192 s.Cron.Remove(j.ID)193 }194 }195 s.Cron.Stop()196 return nil197 })198 // defer s.DB.Close() // nolint: errcheck199 return errg.Wait()200}201202// Close closes the SSH server.203func (s *Server) Close() error {204 var errg errgroup.Group205 errg.Go(s.GitDaemon.Close)206 errg.Go(s.HTTPServer.Close)207 errg.Go(s.SSHServer.Close)208 errg.Go(s.StatsServer.Close)209 errg.Go(func() error {210 s.Cron.Stop()211 return nil212 })213 // defer s.DB.Close() // nolint: errcheck214 return errg.Wait()215}