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