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