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