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