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