server.go

  1package serve
  2
  3import (
  4	"context"
  5	"crypto/tls"
  6	"errors"
  7	"fmt"
  8	"net/http"
  9
 10	"charm.land/log/v2"
 11
 12	"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)
 24
 25// Server is the Soft Serve server.
 26type Server struct {
 27	SSHServer   *sshsrv.SSHServer
 28	GitDaemon   *daemon.GitDaemon
 29	HTTPServer  *web.HTTPServer
 30	StatsServer *stats.StatsServer
 31	CertLoader  *CertReloader
 32	Cron        *cron.Scheduler
 33	Config      *config.Config
 34	Backend     *backend.Backend
 35	DB          *db.DB
 36
 37	logger *log.Logger
 38	ctx    context.Context
 39}
 40
 41// NewServer returns a new *Server configured to serve Soft Serve. The SSH
 42// server key-pair will be created if none exists.
 43// It expects a context with *backend.Backend, *db.DB, *log.Logger, and
 44// *config.Config attached.
 45func NewServer(ctx context.Context) (*Server, error) {
 46	var err error
 47	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	}
 58
 59	// 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		}
 66
 67		j.ID = id
 68	}
 69
 70	srv.Cron = sched
 71
 72	srv.SSHServer, err = sshsrv.NewSSHServer(ctx)
 73	if err != nil {
 74		return nil, fmt.Errorf("create ssh server: %w", err)
 75	}
 76
 77	srv.GitDaemon, err = daemon.NewGitDaemon(ctx)
 78	if err != nil {
 79		return nil, fmt.Errorf("create git daemon: %w", err)
 80	}
 81
 82	srv.HTTPServer, err = web.NewHTTPServer(ctx)
 83	if err != nil {
 84		return nil, fmt.Errorf("create http server: %w", err)
 85	}
 86
 87	srv.StatsServer, err = stats.NewStatsServer(ctx)
 88	if err != nil {
 89		return nil, fmt.Errorf("create stats server: %w", err)
 90	}
 91
 92	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		}
 97
 98		srv.HTTPServer.SetTLSConfig(&tls.Config{
 99			GetCertificate: srv.CertLoader.GetCertificateFunc(),
100		})
101	}
102
103	return srv, nil
104}
105
106// ReloadCertificates reloads the TLS certificates for the HTTP server.
107func (s *Server) ReloadCertificates() error {
108	if s.CertLoader == nil {
109		return nil
110	}
111	return s.CertLoader.Reload()
112}
113
114// Start starts the SSH server.
115func (s *Server) Start() error {
116	errg, _ := errgroup.WithContext(s.ctx)
117
118	// optionally start the SSH server
119	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 err
124			}
125			return nil
126		})
127	}
128
129	// optionally start the git daemon
130	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 err
135			}
136			return nil
137		})
138	}
139
140	// optionally start the HTTP server
141	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 err
146			}
147			return nil
148		})
149	}
150
151	// optionally start the Stats server
152	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 err
157			}
158			return nil
159		})
160	}
161
162	errg.Go(func() error {
163		s.Cron.Start()
164		return nil
165	})
166	return errg.Wait()
167}
168
169// 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 nil
190	})
191	// defer s.DB.Close() // nolint: errcheck
192	return errg.Wait()
193}
194
195// Close closes the SSH server.
196func (s *Server) Close() error {
197	var errg errgroup.Group
198	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 nil
205	})
206	// defer s.DB.Close() // nolint: errcheck
207	return errg.Wait()
208}