soft-serve

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

  1package serve
  2
  3import (
  4	"bytes"
  5	"context"
  6	"crypto/tls"
  7	"errors"
  8	"fmt"
  9	"io"
 10	"net/http"
 11
 12	"charm.land/log/v2"
 13
 14	"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)
 26
 27// Server is the Soft Serve server.
 28type Server struct {
 29	SSHServer   *sshsrv.SSHServer
 30	GitDaemon   *daemon.GitDaemon
 31	HTTPServer  *web.HTTPServer
 32	StatsServer *stats.StatsServer
 33	CertLoader  *CertReloader
 34	Cron        *cron.Scheduler
 35	Config      *config.Config
 36	Backend     *backend.Backend
 37	DB          *db.DB
 38
 39	logger *log.Logger
 40	ctx    context.Context
 41}
 42
 43// NewServer returns a new *Server configured to serve Soft Serve. The SSH
 44// server key-pair will be created if none exists.
 45// It expects a context with *backend.Backend, *db.DB, *log.Logger, and
 46// *config.Config attached.
 47func NewServer(ctx context.Context) (*Server, error) {
 48	var err error
 49	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	}
 60
 61	// 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			continue
 67		}
 68		id, err := sched.AddFunc(spec, func() {
 69			logger.Debug("cron job starting", "job", n)
 70			cmd := j.Runner.Command()
 71
 72			errBuf := bytes.NewBuffer(nil)
 73
 74			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		}
 88
 89		j.ID = id
 90	}
 91
 92	srv.Cron = sched
 93
 94	srv.SSHServer, err = sshsrv.NewSSHServer(ctx)
 95	if err != nil {
 96		return nil, fmt.Errorf("create ssh server: %w", err)
 97	}
 98
 99	srv.GitDaemon, err = daemon.NewGitDaemon(ctx)
100	if err != nil {
101		return nil, fmt.Errorf("create git daemon: %w", err)
102	}
103
104	srv.HTTPServer, err = web.NewHTTPServer(ctx)
105	if err != nil {
106		return nil, fmt.Errorf("create http server: %w", err)
107	}
108
109	srv.StatsServer, err = stats.NewStatsServer(ctx)
110	if err != nil {
111		return nil, fmt.Errorf("create stats server: %w", err)
112	}
113
114	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		}
119
120		srv.HTTPServer.SetTLSConfig(&tls.Config{
121			GetCertificate: srv.CertLoader.GetCertificateFunc(),
122		})
123	}
124
125	return srv, nil
126}
127
128// ReloadCertificates reloads the TLS certificates for the HTTP server.
129func (s *Server) ReloadCertificates() error {
130	if s.CertLoader == nil {
131		return nil
132	}
133	return s.CertLoader.Reload()
134}
135
136// Start starts the SSH server.
137func (s *Server) Start() error {
138	errg, _ := errgroup.WithContext(s.ctx)
139
140	// optionally start the SSH server
141	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 err
146			}
147			return nil
148		})
149	}
150
151	// optionally start the git daemon
152	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 err
157			}
158			return nil
159		})
160	}
161
162	// optionally start the HTTP server
163	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 err
168			}
169			return nil
170		})
171	}
172
173	// optionally start the Stats server
174	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 err
179			}
180			return nil
181		})
182	}
183
184	errg.Go(func() error {
185		s.Cron.Start()
186		return nil
187	})
188	return errg.Wait()
189}
190
191// 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 1
209			if j.ID != 0 {
210				s.Cron.Remove(j.ID)
211			}
212		}
213		s.Cron.Stop()
214		return nil
215	})
216	// defer s.DB.Close() // nolint: errcheck
217	return errg.Wait()
218}
219
220// Close closes the SSH server.
221func (s *Server) Close() error {
222	var errg errgroup.Group
223	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 nil
230	})
231	// defer s.DB.Close() // nolint: errcheck
232	return errg.Wait()
233}