1package server
  2
  3import (
  4	"context"
  5	"net/http"
  6	"path/filepath"
  7
  8	"github.com/charmbracelet/keygen"
  9	"github.com/charmbracelet/log"
 10
 11	"github.com/charmbracelet/soft-serve/server/backend"
 12	"github.com/charmbracelet/soft-serve/server/backend/file"
 13	"github.com/charmbracelet/soft-serve/server/config"
 14	"github.com/charmbracelet/ssh"
 15	"golang.org/x/sync/errgroup"
 16)
 17
 18var (
 19	logger = log.WithPrefix("server")
 20)
 21
 22// Server is the Soft Serve server.
 23type Server struct {
 24	SSHServer   *SSHServer
 25	GitDaemon   *GitDaemon
 26	HTTPServer  *HTTPServer
 27	StatsServer *StatsServer
 28	Config      *config.Config
 29	Backend     backend.Backend
 30}
 31
 32// NewServer returns a new *ssh.Server configured to serve Soft Serve. The SSH
 33// server key-pair will be created if none exists. An initial admin SSH public
 34// key can be provided with authKey. If authKey is provided, access will be
 35// restricted to that key. If authKey is not provided, the server will be
 36// publicly writable until configured otherwise by cloning the `config` repo.
 37func NewServer(cfg *config.Config) (*Server, error) {
 38	var err error
 39	if cfg.Backend == nil {
 40		fb, err := file.NewFileBackend(cfg.DataPath)
 41		if err != nil {
 42			logger.Fatal(err)
 43		}
 44		// Add the initial admin keys to the list of admins.
 45		fb.AdditionalAdmins = cfg.InitialAdminKeys
 46		cfg = cfg.WithBackend(fb)
 47
 48		// Create internal key.
 49		_, err = keygen.NewWithWrite(
 50			filepath.Join(cfg.DataPath, cfg.SSH.InternalKeyPath),
 51			nil,
 52			keygen.Ed25519,
 53		)
 54		if err != nil {
 55			return nil, err
 56		}
 57	}
 58
 59	srv := &Server{
 60		Config:  cfg,
 61		Backend: cfg.Backend,
 62	}
 63	srv.SSHServer, err = NewSSHServer(cfg, srv)
 64	if err != nil {
 65		return nil, err
 66	}
 67
 68	srv.GitDaemon, err = NewGitDaemon(cfg)
 69	if err != nil {
 70		return nil, err
 71	}
 72
 73	srv.HTTPServer, err = NewHTTPServer(cfg)
 74	if err != nil {
 75		return nil, err
 76	}
 77
 78	srv.StatsServer, err = NewStatsServer(cfg)
 79	if err != nil {
 80		return nil, err
 81	}
 82
 83	return srv, nil
 84}
 85
 86// Start starts the SSH server.
 87func (s *Server) Start() error {
 88	var errg errgroup.Group
 89	errg.Go(func() error {
 90		log.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)
 91		if err := s.GitDaemon.Start(); err != ErrServerClosed {
 92			return err
 93		}
 94		return nil
 95	})
 96	errg.Go(func() error {
 97		log.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
 98		if err := s.HTTPServer.ListenAndServe(); err != http.ErrServerClosed {
 99			return err
100		}
101		return nil
102	})
103	errg.Go(func() error {
104		log.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
105		if err := s.SSHServer.ListenAndServe(); err != ssh.ErrServerClosed {
106			return err
107		}
108		return nil
109	})
110	errg.Go(func() error {
111		log.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)
112		if err := s.StatsServer.ListenAndServe(); err != http.ErrServerClosed {
113			return err
114		}
115		return nil
116	})
117	return errg.Wait()
118}
119
120// Shutdown lets the server gracefully shutdown.
121func (s *Server) Shutdown(ctx context.Context) error {
122	var errg errgroup.Group
123	errg.Go(func() error {
124		return s.GitDaemon.Shutdown(ctx)
125	})
126	errg.Go(func() error {
127		return s.HTTPServer.Shutdown(ctx)
128	})
129	errg.Go(func() error {
130		return s.SSHServer.Shutdown(ctx)
131	})
132	errg.Go(func() error {
133		return s.StatsServer.Shutdown(ctx)
134	})
135	return errg.Wait()
136}
137
138// Close closes the SSH server.
139func (s *Server) Close() error {
140	var errg errgroup.Group
141	errg.Go(s.GitDaemon.Close)
142	errg.Go(s.HTTPServer.Close)
143	errg.Go(s.SSHServer.Close)
144	errg.Go(s.StatsServer.Close)
145	return errg.Wait()
146}