1package serve23import (4 "bytes"5 "context"6 "crypto/tls"7 "errors"8 "fmt"9 "io"10 "net/http"1112 "charm.land/log/v2"1314 "github.com/charmbracelet/soft-serve/pkg/backend"15 "github.com/charmbracelet/soft-serve/pkg/config"16 "github.com/charmbracelet/soft-serve/pkg/cron"17 "github.com/charmbracelet/soft-serve/pkg/daemon"18 "github.com/charmbracelet/soft-serve/pkg/db"19 "github.com/charmbracelet/soft-serve/pkg/jobs"20 sshsrv "github.com/charmbracelet/soft-serve/pkg/ssh"21 "github.com/charmbracelet/soft-serve/pkg/stats"22 "github.com/charmbracelet/soft-serve/pkg/web"23 "github.com/charmbracelet/ssh"24 "golang.org/x/sync/errgroup"25)2627// Server is the Soft Serve server.28type Server struct {29 SSHServer *sshsrv.SSHServer30 GitDaemon *daemon.GitDaemon31 HTTPServer *web.HTTPServer32 StatsServer *stats.StatsServer33 CertLoader *CertReloader34 Cron *cron.Scheduler35 Config *config.Config36 Backend *backend.Backend37 DB *db.DB3839 logger *log.Logger40 ctx context.Context41}4243// NewServer returns a new *Server configured to serve Soft Serve. The SSH44// server key-pair will be created if none exists.45// It expects a context with *backend.Backend, *db.DB, *log.Logger, and46// *config.Config attached.47func NewServer(ctx context.Context) (*Server, error) {48 var err error49 cfg := config.FromContext(ctx)50 be := backend.FromContext(ctx)51 db := db.FromContext(ctx)52 logger := log.FromContext(ctx).WithPrefix("server")53 srv := &Server{54 Config: cfg,55 Backend: be,56 DB: db,57 logger: log.FromContext(ctx).WithPrefix("server"),58 ctx: ctx,59 }6061 // Add cron jobs.62 sched := cron.NewScheduler(ctx)63 for n, j := range jobs.List() {64 spec := j.Runner.Spec(ctx)65 if spec == "" {66 continue67 }68 id, err := sched.AddFunc(spec, func() {69 logger.Debug("cron job starting", "job", n)70 cmd := j.Runner.Command()7172 errBuf := bytes.NewBuffer(nil)7374 cmd.SetContext(ctx)75 cmd.SetArgs([]string{})76 cmd.SetIn(bytes.NewReader(nil))77 cmd.SetOut(io.Discard)78 cmd.SetErr(errBuf)79 if err := cmd.ExecuteContext(ctx); err != nil {80 logger.Warn("cron job executed failed",81 "job", n, "err", err, "stderr", errBuf.String())82 }83 logger.Debug("cron job ended", "job", n)84 })85 if err != nil {86 logger.Warn("error adding cron job", "job", n, "err", err)87 }8889 j.ID = id90 }9192 srv.Cron = sched9394 srv.SSHServer, err = sshsrv.NewSSHServer(ctx)95 if err != nil {96 return nil, fmt.Errorf("create ssh server: %w", err)97 }9899 srv.GitDaemon, err = daemon.NewGitDaemon(ctx)100 if err != nil {101 return nil, fmt.Errorf("create git daemon: %w", err)102 }103104 srv.HTTPServer, err = web.NewHTTPServer(ctx)105 if err != nil {106 return nil, fmt.Errorf("create http server: %w", err)107 }108109 srv.StatsServer, err = stats.NewStatsServer(ctx)110 if err != nil {111 return nil, fmt.Errorf("create stats server: %w", err)112 }113114 if cfg.HTTP.TLSKeyPath != "" && cfg.HTTP.TLSCertPath != "" {115 srv.CertLoader, err = NewCertReloader(cfg.HTTP.TLSCertPath, cfg.HTTP.TLSKeyPath, logger)116 if err != nil {117 return nil, fmt.Errorf("create cert reloader: %w", err)118 }119120 srv.HTTPServer.SetTLSConfig(&tls.Config{121 GetCertificate: srv.CertLoader.GetCertificateFunc(),122 })123 }124125 return srv, nil126}127128// ReloadCertificates reloads the TLS certificates for the HTTP server.129func (s *Server) ReloadCertificates() error {130 if s.CertLoader == nil {131 return nil132 }133 return s.CertLoader.Reload()134}135136// Start starts the SSH server.137func (s *Server) Start() error {138 errg, _ := errgroup.WithContext(s.ctx)139140 // optionally start the SSH server141 if s.Config.SSH.Enabled {142 errg.Go(func() error {143 s.logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)144 if err := s.SSHServer.ListenAndServe(); !errors.Is(err, ssh.ErrServerClosed) {145 return err146 }147 return nil148 })149 }150151 // optionally start the git daemon152 if s.Config.Git.Enabled {153 errg.Go(func() error {154 s.logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)155 if err := s.GitDaemon.ListenAndServe(); !errors.Is(err, daemon.ErrServerClosed) {156 return err157 }158 return nil159 })160 }161162 // optionally start the HTTP server163 if s.Config.HTTP.Enabled {164 errg.Go(func() error {165 s.logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)166 if err := s.HTTPServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {167 return err168 }169 return nil170 })171 }172173 // optionally start the Stats server174 if s.Config.Stats.Enabled {175 errg.Go(func() error {176 s.logger.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)177 if err := s.StatsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {178 return err179 }180 return nil181 })182 }183184 errg.Go(func() error {185 s.Cron.Start()186 return nil187 })188 return errg.Wait()189}190191// Shutdown lets the server gracefully shutdown.192func (s *Server) Shutdown(ctx context.Context) error {193 errg, ctx := errgroup.WithContext(ctx)194 errg.Go(func() error {195 return s.GitDaemon.Shutdown(ctx)196 })197 errg.Go(func() error {198 return s.HTTPServer.Shutdown(ctx)199 })200 errg.Go(func() error {201 return s.SSHServer.Shutdown(ctx)202 })203 errg.Go(func() error {204 return s.StatsServer.Shutdown(ctx)205 })206 errg.Go(func() error {207 for _, j := range jobs.List() {208 // jobID from github.com/robfig/cron/v2 starts from 1209 if j.ID != 0 {210 s.Cron.Remove(j.ID)211 }212 }213 s.Cron.Stop()214 return nil215 })216 // defer s.DB.Close() // nolint: errcheck217 return errg.Wait()218}219220// Close closes the SSH server.221func (s *Server) Close() error {222 var errg errgroup.Group223 errg.Go(s.GitDaemon.Close)224 errg.Go(s.HTTPServer.Close)225 errg.Go(s.SSHServer.Close)226 errg.Go(s.StatsServer.Close)227 errg.Go(func() error {228 s.Cron.Stop()229 return nil230 })231 // defer s.DB.Close() // nolint: errcheck232 return errg.Wait()233}