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 id, err := sched.AddFunc(j.Runner.Spec(ctx), j.Runner.Func(ctx))63 if err != nil {64 logger.Warn("error adding cron job", "job", n, "err", err)65 }6667 j.ID = id68 }6970 srv.Cron = sched7172 srv.SSHServer, err = sshsrv.NewSSHServer(ctx)73 if err != nil {74 return nil, fmt.Errorf("create ssh server: %w", err)75 }7677 srv.GitDaemon, err = daemon.NewGitDaemon(ctx)78 if err != nil {79 return nil, fmt.Errorf("create git daemon: %w", err)80 }8182 srv.HTTPServer, err = web.NewHTTPServer(ctx)83 if err != nil {84 return nil, fmt.Errorf("create http server: %w", err)85 }8687 srv.StatsServer, err = stats.NewStatsServer(ctx)88 if err != nil {89 return nil, fmt.Errorf("create stats server: %w", err)90 }9192 if cfg.HTTP.TLSKeyPath != "" && cfg.HTTP.TLSCertPath != "" {93 srv.CertLoader, err = NewCertReloader(cfg.HTTP.TLSCertPath, cfg.HTTP.TLSKeyPath, logger)94 if err != nil {95 return nil, fmt.Errorf("create cert reloader: %w", err)96 }9798 srv.HTTPServer.SetTLSConfig(&tls.Config{99 GetCertificate: srv.CertLoader.GetCertificateFunc(),100 })101 }102103 return srv, nil104}105106// ReloadCertificates reloads the TLS certificates for the HTTP server.107func (s *Server) ReloadCertificates() error {108 if s.CertLoader == nil {109 return nil110 }111 return s.CertLoader.Reload()112}113114// Start starts the SSH server.115func (s *Server) Start() error {116 errg, _ := errgroup.WithContext(s.ctx)117118 // optionally start the SSH server119 if s.Config.SSH.Enabled {120 errg.Go(func() error {121 s.logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)122 if err := s.SSHServer.ListenAndServe(); !errors.Is(err, ssh.ErrServerClosed) {123 return err124 }125 return nil126 })127 }128129 // optionally start the git daemon130 if s.Config.Git.Enabled {131 errg.Go(func() error {132 s.logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)133 if err := s.GitDaemon.ListenAndServe(); !errors.Is(err, daemon.ErrServerClosed) {134 return err135 }136 return nil137 })138 }139140 // optionally start the HTTP server141 if s.Config.HTTP.Enabled {142 errg.Go(func() error {143 s.logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)144 if err := s.HTTPServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {145 return err146 }147 return nil148 })149 }150151 // optionally start the Stats server152 if s.Config.Stats.Enabled {153 errg.Go(func() error {154 s.logger.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)155 if err := s.StatsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {156 return err157 }158 return nil159 })160 }161162 errg.Go(func() error {163 s.Cron.Start()164 return nil165 })166 return errg.Wait()167}168169// Shutdown lets the server gracefully shutdown.170func (s *Server) Shutdown(ctx context.Context) error {171 errg, ctx := errgroup.WithContext(ctx)172 errg.Go(func() error {173 return s.GitDaemon.Shutdown(ctx)174 })175 errg.Go(func() error {176 return s.HTTPServer.Shutdown(ctx)177 })178 errg.Go(func() error {179 return s.SSHServer.Shutdown(ctx)180 })181 errg.Go(func() error {182 return s.StatsServer.Shutdown(ctx)183 })184 errg.Go(func() error {185 for _, j := range jobs.List() {186 s.Cron.Remove(j.ID)187 }188 s.Cron.Stop()189 return nil190 })191 // defer s.DB.Close() // nolint: errcheck192 return errg.Wait()193}194195// Close closes the SSH server.196func (s *Server) Close() error {197 var errg errgroup.Group198 errg.Go(s.GitDaemon.Close)199 errg.Go(s.HTTPServer.Close)200 errg.Go(s.SSHServer.Close)201 errg.Go(s.StatsServer.Close)202 errg.Go(func() error {203 s.Cron.Stop()204 return nil205 })206 // defer s.DB.Close() // nolint: errcheck207 return errg.Wait()208}