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