1package serve
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "net/http"
8
9 log "github.com/charmbracelet/log/v2"
10
11 "github.com/charmbracelet/soft-serve/pkg/backend"
12 "github.com/charmbracelet/soft-serve/pkg/config"
13 "github.com/charmbracelet/soft-serve/pkg/cron"
14 "github.com/charmbracelet/soft-serve/pkg/daemon"
15 "github.com/charmbracelet/soft-serve/pkg/db"
16 "github.com/charmbracelet/soft-serve/pkg/jobs"
17 sshsrv "github.com/charmbracelet/soft-serve/pkg/ssh"
18 "github.com/charmbracelet/soft-serve/pkg/stats"
19 "github.com/charmbracelet/soft-serve/pkg/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.Scheduler
31 Config *config.Config
32 Backend *backend.Backend
33 DB *db.DB
34
35 logger *log.Logger
36 ctx context.Context
37}
38
39// NewServer returns a new *Server configured to serve Soft Serve. The SSH
40// server key-pair will be created if none exists.
41// It expects a context with *backend.Backend, *db.DB, *log.Logger, and
42// *config.Config attached.
43func NewServer(ctx context.Context) (*Server, error) {
44 var err error
45 cfg := config.FromContext(ctx)
46 be := backend.FromContext(ctx)
47 db := db.FromContext(ctx)
48 logger := log.FromContext(ctx).WithPrefix("server")
49 srv := &Server{
50 Config: cfg,
51 Backend: be,
52 DB: db,
53 logger: log.FromContext(ctx).WithPrefix("server"),
54 ctx: ctx,
55 }
56
57 // Add cron jobs.
58 sched := cron.NewScheduler(ctx)
59 for n, j := range jobs.List() {
60 id, err := sched.AddFunc(j.Runner.Spec(ctx), j.Runner.Func(ctx))
61 if err != nil {
62 logger.Warn("error adding cron job", "job", n, "err", err)
63 }
64
65 j.ID = id
66 }
67
68 srv.Cron = sched
69
70 srv.SSHServer, err = sshsrv.NewSSHServer(ctx)
71 if err != nil {
72 return nil, fmt.Errorf("create ssh server: %w", err)
73 }
74
75 srv.GitDaemon, err = daemon.NewGitDaemon(ctx)
76 if err != nil {
77 return nil, fmt.Errorf("create git daemon: %w", err)
78 }
79
80 srv.HTTPServer, err = web.NewHTTPServer(ctx)
81 if err != nil {
82 return nil, fmt.Errorf("create http server: %w", err)
83 }
84
85 srv.StatsServer, err = stats.NewStatsServer(ctx)
86 if err != nil {
87 return nil, fmt.Errorf("create stats server: %w", err)
88 }
89
90 return srv, nil
91}
92
93// Start starts the SSH server.
94func (s *Server) Start() error {
95 errg, _ := errgroup.WithContext(s.ctx)
96
97 // optionally start the SSH server
98 if s.Config.SSH.Enabled {
99 errg.Go(func() error {
100 s.logger.Print("Starting SSH server", "addr", s.Config.SSH.ListenAddr)
101 if err := s.SSHServer.ListenAndServe(); !errors.Is(err, ssh.ErrServerClosed) {
102 return err
103 }
104 return nil
105 })
106 }
107
108 // optionally start the git daemon
109 if s.Config.Git.Enabled {
110 errg.Go(func() error {
111 s.logger.Print("Starting Git daemon", "addr", s.Config.Git.ListenAddr)
112 if err := s.GitDaemon.ListenAndServe(); !errors.Is(err, daemon.ErrServerClosed) {
113 return err
114 }
115 return nil
116 })
117 }
118
119 // optionally start the HTTP server
120 if s.Config.HTTP.Enabled {
121 errg.Go(func() error {
122 s.logger.Print("Starting HTTP server", "addr", s.Config.HTTP.ListenAddr)
123 if err := s.HTTPServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
124 return err
125 }
126 return nil
127 })
128 }
129
130 // optionally start the Stats server
131 if s.Config.Stats.Enabled {
132 errg.Go(func() error {
133 s.logger.Print("Starting Stats server", "addr", s.Config.Stats.ListenAddr)
134 if err := s.StatsServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
135 return err
136 }
137 return nil
138 })
139 }
140
141 errg.Go(func() error {
142 s.Cron.Start()
143 return nil
144 })
145 return errg.Wait()
146}
147
148// Shutdown lets the server gracefully shutdown.
149func (s *Server) Shutdown(ctx context.Context) error {
150 errg, ctx := errgroup.WithContext(ctx)
151 errg.Go(func() error {
152 return s.GitDaemon.Shutdown(ctx)
153 })
154 errg.Go(func() error {
155 return s.HTTPServer.Shutdown(ctx)
156 })
157 errg.Go(func() error {
158 return s.SSHServer.Shutdown(ctx)
159 })
160 errg.Go(func() error {
161 return s.StatsServer.Shutdown(ctx)
162 })
163 errg.Go(func() error {
164 for _, j := range jobs.List() {
165 s.Cron.Remove(j.ID)
166 }
167 s.Cron.Stop()
168 return nil
169 })
170 // defer s.DB.Close() //nolint: errcheck
171 return errg.Wait()
172}
173
174// Close closes the SSH server.
175func (s *Server) Close() error {
176 var errg errgroup.Group
177 errg.Go(s.GitDaemon.Close)
178 errg.Go(s.HTTPServer.Close)
179 errg.Go(s.SSHServer.Close)
180 errg.Go(s.StatsServer.Close)
181 errg.Go(func() error {
182 s.Cron.Stop()
183 return nil
184 })
185 // defer s.DB.Close() //nolint: errcheck
186 return errg.Wait()
187}