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