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