soft-serve

git clone git://git.lin.moe/fork/soft-serve.git

  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		spec := j.Runner.Spec(ctx)
 63		if spec == "" {
 64			continue
 65		}
 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		}
 70
 71		j.ID = id
 72	}
 73
 74	srv.Cron = sched
 75
 76	srv.SSHServer, err = sshsrv.NewSSHServer(ctx)
 77	if err != nil {
 78		return nil, fmt.Errorf("create ssh server: %w", err)
 79	}
 80
 81	srv.GitDaemon, err = daemon.NewGitDaemon(ctx)
 82	if err != nil {
 83		return nil, fmt.Errorf("create git daemon: %w", err)
 84	}
 85
 86	srv.HTTPServer, err = web.NewHTTPServer(ctx)
 87	if err != nil {
 88		return nil, fmt.Errorf("create http server: %w", err)
 89	}
 90
 91	srv.StatsServer, err = stats.NewStatsServer(ctx)
 92	if err != nil {
 93		return nil, fmt.Errorf("create stats server: %w", err)
 94	}
 95
 96	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		}
101
102		srv.HTTPServer.SetTLSConfig(&tls.Config{
103			GetCertificate: srv.CertLoader.GetCertificateFunc(),
104		})
105	}
106
107	return srv, nil
108}
109
110// ReloadCertificates reloads the TLS certificates for the HTTP server.
111func (s *Server) ReloadCertificates() error {
112	if s.CertLoader == nil {
113		return nil
114	}
115	return s.CertLoader.Reload()
116}
117
118// Start starts the SSH server.
119func (s *Server) Start() error {
120	errg, _ := errgroup.WithContext(s.ctx)
121
122	// optionally start the SSH server
123	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 err
128			}
129			return nil
130		})
131	}
132
133	// optionally start the git daemon
134	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 err
139			}
140			return nil
141		})
142	}
143
144	// optionally start the HTTP server
145	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 err
150			}
151			return nil
152		})
153	}
154
155	// optionally start the Stats server
156	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 err
161			}
162			return nil
163		})
164	}
165
166	errg.Go(func() error {
167		s.Cron.Start()
168		return nil
169	})
170	return errg.Wait()
171}
172
173// 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 1
191			if j.ID != 0 {
192				s.Cron.Remove(j.ID)
193			}
194		}
195		s.Cron.Stop()
196		return nil
197	})
198	// defer s.DB.Close() // nolint: errcheck
199	return errg.Wait()
200}
201
202// Close closes the SSH server.
203func (s *Server) Close() error {
204	var errg errgroup.Group
205	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 nil
212	})
213	// defer s.DB.Close() // nolint: errcheck
214	return errg.Wait()
215}