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