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