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