1package server
  2
  3import (
  4	"context"
  5	"errors"
  6	"fmt"
  7	"net/http"
  8
  9	"github.com/charmbracelet/log"
 10
 11	"github.com/charmbracelet/soft-serve/server/backend"
 12	"github.com/charmbracelet/soft-serve/server/backend/sqlite"
 13	"github.com/charmbracelet/soft-serve/server/config"
 14	"github.com/charmbracelet/soft-serve/server/cron"
 15	"github.com/charmbracelet/soft-serve/server/daemon"
 16	"github.com/charmbracelet/soft-serve/server/internal"
 17	sshsrv "github.com/charmbracelet/soft-serve/server/ssh"
 18	"github.com/charmbracelet/soft-serve/server/stats"
 19	"github.com/charmbracelet/soft-serve/server/web"
 20	"github.com/charmbracelet/ssh"
 21	"golang.org/x/sync/errgroup"
 22)
 23
 24var (
 25	logger = log.WithPrefix("server")
 26)
 27
 28// Server is the Soft Serve server.
 29type Server struct {
 30	SSHServer      *sshsrv.SSHServer
 31	GitDaemon      *daemon.GitDaemon
 32	HTTPServer     *web.HTTPServer
 33	StatsServer    *stats.StatsServer
 34	InternalServer *internal.InternalServer
 35	Cron           *cron.CronScheduler
 36	Config         *config.Config
 37	Backend        backend.Backend
 38	ctx            context.Context
 39}
 40
 41// NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
 42// server key-pair will be created if none exists. An initial admin SSH public
 43// key can be provided with authKey. If authKey is provided, access will be
 44// restricted to that key. If authKey is not provided, the server will be
 45// publicly writable until configured otherwise by cloning the `config` repo.
 46func NewServer(ctx context.Context, cfg *config.Config) (*Server, error) {
 47	var err error
 48	if cfg.Backend == nil {
 49		sb, err := sqlite.NewSqliteBackend(ctx, cfg)
 50		if err != nil {
 51			return nil, fmt.Errorf("create backend: %w", err)
 52		}
 53
 54		cfg = cfg.WithBackend(sb)
 55	}
 56
 57	srv := &Server{
 58		Cron:    cron.NewCronScheduler(ctx),
 59		Config:  cfg,
 60		Backend: cfg.Backend,
 61		ctx:     ctx,
 62	}
 63
 64	// Add cron jobs.
 65	srv.Cron.AddFunc(jobSpecs["mirror"], mirrorJob(cfg))
 66
 67	srv.SSHServer, err = sshsrv.NewSSHServer(cfg)
 68	if err != nil {
 69		return nil, fmt.Errorf("create ssh server: %w", err)
 70	}
 71
 72	srv.GitDaemon, err = daemon.NewGitDaemon(cfg)
 73	if err != nil {
 74		return nil, fmt.Errorf("create git daemon: %w", err)
 75	}
 76
 77	srv.HTTPServer, err = web.NewHTTPServer(cfg)
 78	if err != nil {
 79		return nil, fmt.Errorf("create http server: %w", err)
 80	}
 81
 82	srv.StatsServer, err = stats.NewStatsServer(cfg)
 83	if err != nil {
 84		return nil, fmt.Errorf("create stats server: %w", err)
 85	}
 86
 87	srv.InternalServer, err = internal.NewInternalServer(cfg, srv)
 88	if err != nil {
 89		return nil, fmt.Errorf("create internal server: %w", err)
 90	}
 91
 92	return srv, nil
 93}
 94
 95func start(ctx context.Context, fn func() error) error {
 96	errc := make(chan error, 1)
 97	go func() {
 98		errc <- fn()
 99	}()
100
101	select {
102	case err := <-errc:
103		return err
104	case <-ctx.Done():
105		return ctx.Err()
106	}
107}
108
109// Start starts the SSH server.
110func (s *Server) Start() error {
111	logger := log.FromContext(s.ctx).WithPrefix("server")
112	errg, ctx := errgroup.WithContext(s.ctx)
113	errg.Go(func() error {
114		logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)
115		if err := start(ctx, s.GitDaemon.Start); !errors.Is(err, daemon.ErrServerClosed) {
116			return err
117		}
118		return nil
119	})
120	errg.Go(func() error {
121		logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
122		if err := start(ctx, s.HTTPServer.ListenAndServe); !errors.Is(err, http.ErrServerClosed) {
123			return err
124		}
125		return nil
126	})
127	errg.Go(func() error {
128		logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
129		if err := start(ctx, s.SSHServer.ListenAndServe); !errors.Is(err, ssh.ErrServerClosed) {
130			return err
131		}
132		return nil
133	})
134	errg.Go(func() error {
135		logger.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)
136		if err := start(ctx, s.StatsServer.ListenAndServe); !errors.Is(err, http.ErrServerClosed) {
137			return err
138		}
139		return nil
140	})
141	errg.Go(func() error {
142		logger.Print("Starting cron scheduler")
143		s.Cron.Start()
144		return nil
145	})
146	errg.Go(func() error {
147		logger.Print("Starting internal server", "addr", s.Config.Internal.ListenAddr)
148		if err := start(ctx, s.InternalServer.Start); !errors.Is(err, http.ErrServerClosed) {
149			return err
150		}
151		return nil
152	})
153	return errg.Wait()
154}
155
156// Shutdown lets the server gracefully shutdown.
157func (s *Server) Shutdown(ctx context.Context) error {
158	var errg errgroup.Group
159	errg.Go(func() error {
160		return s.GitDaemon.Shutdown(ctx)
161	})
162	errg.Go(func() error {
163		return s.HTTPServer.Shutdown(ctx)
164	})
165	errg.Go(func() error {
166		return s.SSHServer.Shutdown(ctx)
167	})
168	errg.Go(func() error {
169		return s.StatsServer.Shutdown(ctx)
170	})
171	errg.Go(func() error {
172		s.Cron.Stop()
173		return nil
174	})
175	errg.Go(func() error {
176		return s.InternalServer.Shutdown(ctx)
177	})
178	return errg.Wait()
179}
180
181// Close closes the SSH server.
182func (s *Server) Close() error {
183	var errg errgroup.Group
184	errg.Go(s.GitDaemon.Close)
185	errg.Go(s.HTTPServer.Close)
186	errg.Go(s.SSHServer.Close)
187	errg.Go(s.StatsServer.Close)
188	errg.Go(func() error {
189		s.Cron.Stop()
190		return nil
191	})
192	errg.Go(s.InternalServer.Close)
193	return errg.Wait()
194}