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