server.go

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