Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/server.go

Line attribution on main · 435 line(s)

a5f19b3c
Keenan Nemetz
17d ago
1 // Package server wires the components together and owns the listeners.
653e0cfc
Keenan Nemetz
17d ago
2 package server
3
4 import (
a5f19b3c
Keenan Nemetz
17d ago
5 "context"
6 "errors"
653e0cfc
Keenan Nemetz
17d ago
7 "fmt"
8 "log/slog"
a5f19b3c
Keenan Nemetz
17d ago
9 "net"
653e0cfc
Keenan Nemetz
17d ago
10 "net/http"
a5f19b3c
Keenan Nemetz
17d ago
11 "strconv"
642e5994
Keenan Nemetz
14d ago
12 "strings"
a5f19b3c
Keenan Nemetz
17d ago
13 "time"
653e0cfc
Keenan Nemetz
17d ago
14
d37d301f
Keenan Nemetz
15h ago
15 "rodent.software/vole/ci/runs"
16 "rodent.software/vole/core/lfs"
17 "rodent.software/vole/forge"
18 "rodent.software/vole/forge/config"
19 "rodent.software/vole/forge/identity"
20 "rodent.software/vole/forge/index"
21 "rodent.software/vole/forge/instancerepo"
22 "rodent.software/vole/forge/pageview"
23 "rodent.software/vole/forge/repo"
24 "rodent.software/vole/forge/secret"
25 "rodent.software/vole/forge/session"
26 "rodent.software/vole/internal/compress"
27 "rodent.software/vole/server/githttp"
28 "rodent.software/vole/server/pages"
29 "rodent.software/vole/server/ssh"
30 "rodent.software/vole/server/web"
653e0cfc
Keenan Nemetz
17d ago
31 )
32
33 type Server struct {
4506a4d3
Keenan Nemetz
13d ago
34 ctx context.Context
35 cancel context.CancelFunc
36 Cfg *config.Config
37 Repos *repo.Store
38 Identity *identity.Store
39 Sessions *session.Store
40 Runs *runs.Store
41 Index *index.Store
42 LFS *lfs.Store
43 Locks *lfs.LockStore
44 PageViews *pageview.Store
6ce635a8
Keenan Nemetz
1d ago
45 // InstanceRepo is the repository whose tree customizes the instance.
46 InstanceRepo *instancerepo.Store
0c40a1d3
Keenan Nemetz
7d ago
47 // Secrets is the per-repository store behind the settings page and the
48 // `vole secret` verbs. Values never leave it except to a caller it has audited.
49 Secrets *secret.Store
50 Pages *pages.Server
51 Proxy *githttp.Proxy
52 SSH *ssh.Server
53 Web *web.Web
54 Log *slog.Logger
55 mux *http.ServeMux
56 acme *acmeSetup
eb07ff56
Keenan Nemetz
8d ago
57 // metadataWake nudges the metadata event loop after a browser write. Pushes
58 // deliver the same information through the spool; this is the path a write made
59 // without a push uses.
60 metadataWake chan struct{}
653e0cfc
Keenan Nemetz
17d ago
61 }
62
63 func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error) {
64 if log == nil {
65 log = slog.Default()
66 }
ed181212
Keenan Nemetz
13d ago
67 // Refuse an unsafe configuration before anything is opened: a listener that
68 // must not be reachable, a development login next to a public URL, a size that
69 // cannot be satisfied. A warning here would be read by nobody.
70 if err := cfg.Validate(); err != nil {
71 return nil, err
72 }
c5902451
Keenan Nemetz
16d ago
73 inst, err := forge.Open(cfg, binPath)
b44cbfaf
Keenan Nemetz
17d ago
74 if err != nil {
c5902451
Keenan Nemetz
16d ago
75 return nil, err
bf1e027c
Keenan Nemetz
16d ago
76 }
ed181212
Keenan Nemetz
13d ago
77 // A secret that is group- or world-readable is a secret already disclosed.
78 // Tighten what an older install left, say so, and refuse to run when the mode
79 // cannot be changed: an instance that cannot protect its keys must not serve.
80 if tightened, err := forge.TightenSecrets(cfg); err != nil {
81 inst.Close()
82 return nil, err
83 } else if len(tightened) > 0 {
84 log.Warn("tightened permissions on secret files", "paths", strings.Join(tightened, ", "))
85 }
c5902451
Keenan Nemetz
16d ago
86 if inst.ImportedACLs > 0 {
87 log.Info("access rules imported", "repos", inst.ImportedACLs)
ec5c519c
Keenan Nemetz
16d ago
88 }
c5902451
Keenan Nemetz
16d ago
89 if n, err := inst.Repos.InstallAllHooks(); err != nil {
653e0cfc
Keenan Nemetz
17d ago
90 log.Warn("install hooks", "err", err)
a5f19b3c
Keenan Nemetz
17d ago
91 } else if n > 0 {
653e0cfc
Keenan Nemetz
17d ago
92 log.Info("hooks installed", "repos", n)
93 }
642e5994
Keenan Nemetz
14d ago
94 acme, err := newACME(cfg, inst.Repos.Public)
95 if err != nil {
96 inst.Close()
97 return nil, err
98 }
99 if acme != nil {
100 log.Info("automatic HTTPS enabled", "addr", cfg.HTTPSAddr, "hosts", strings.Join(acme.hostNames(), ", "), "cache", cfg.ACMECachePath())
101 if cfg.PagesDomain != "" {
102 log.Info("site certificates issued on demand", "domain", cfg.PagesDomain)
103 }
104 }
c5902451
Keenan Nemetz
16d ago
105 proxy := githttp.New(githttp.Config{
86991e70
Keenan Nemetz
13d ago
106 Root: cfg.ReposDir(),
107 Open: inst.Repos.Open,
108 Log: log,
109 MaxConcurrent: cfg.GitHTTPMaxConcurrent,
110 MaxPerClient: cfg.GitHTTPMaxPerClient,
111 ClientIPHeader: cfg.GitHTTPClientIPHeader,
112 RequestTimeout: time.Duration(cfg.GitHTTPTimeoutSeconds) * time.Second,
653e0cfc
Keenan Nemetz
17d ago
113 })
c5902451
Keenan Nemetz
16d ago
114 ssh, err := ssh.New(ssh.Config{
9e8cc155
Keenan Nemetz
12d ago
115 Identity: inst.Identity,
116 Repos: inst.Repos,
117 Instance: inst,
5ece70a5
Keenan Nemetz
10d ago
118 Logs: inst.Logs,
9e8cc155
Keenan Nemetz
12d ago
119 LFS: inst.LFS,
120 LFSWarnBytes: cfg.LFSWarnBytes,
121 Log: log,
122 HostKeyPath: cfg.SSHHostKey(),
123 MaxBlobBytes: cfg.MaxBlobBytes,
124 MaxRefs: cfg.RepoMaxRefs,
125 MaxMetadataEntries: cfg.RepoMaxMetadataEntries,
126 MaxMetadataBytes: cfg.RepoMaxMetadataBytes,
127 MaxRepoBytes: cfg.RepoMaxBytes,
128 MaxLFSBytes: cfg.RepoMaxLFSBytes,
129 AuditPath: cfg.AuditLog(),
130 SpoolDir: cfg.SpoolDir(),
131 RunsDB: cfg.RunsDB(),
dced06da
Keenan Nemetz
9d ago
132 IndexDB: cfg.IndexDB(),
9e8cc155
Keenan Nemetz
12d ago
133 Runs: inst.Runs,
134 Locks: inst.Locks,
135 Lease: cfg.Lease(),
136 MaxJobsPerRepo: cfg.MaxJobsPerRepo,
137 PagesRef: cfg.PagesRef,
138 PublicURL: cfg.BaseURL(),
139 MaxConnections: cfg.SSHMaxConnections,
140 MaxGitProcesses: cfg.SSHMaxGitProcesses,
a5f19b3c
Keenan Nemetz
17d ago
141 })
142 if err != nil {
c5902451
Keenan Nemetz
16d ago
143 inst.Close()
a5f19b3c
Keenan Nemetz
17d ago
144 return nil, fmt.Errorf("ssh: %w", err)
145 }
c5902451
Keenan Nemetz
16d ago
146 wp, err := web.New(inst.Repos, inst.Index, inst.Identity, inst.Runs, cfg)
653e0cfc
Keenan Nemetz
17d ago
147 if err != nil {
c5902451
Keenan Nemetz
16d ago
148 inst.Close()
653e0cfc
Keenan Nemetz
17d ago
149 return nil, err
150 }
c5902451
Keenan Nemetz
16d ago
151 wp.LFS = inst.LFS
152 wp.Locks = inst.Locks
6c84d387
Keenan Nemetz
15d ago
153 wp.Sessions = inst.Sessions
4506a4d3
Keenan Nemetz
13d ago
154 wp.PageViews = inst.PageViews
6ce635a8
Keenan Nemetz
1d ago
155 wp.InstanceRepo = inst.InstanceRepo
0c40a1d3
Keenan Nemetz
7d ago
156 wp.Secrets = inst.Secrets
653e0cfc
Keenan Nemetz
17d ago
157 wp.Logger = func(format string, args ...any) { log.Warn(fmt.Sprintf(format, args...)) }
f09ef39e
Keenan Nemetz
15d ago
158 if cfg.DevLogin != "" {
159 log.Warn("development login enabled", "account", cfg.DevLogin, "http_addr", cfg.HTTPAddr)
160 }
6c84d387
Keenan Nemetz
15d ago
161 if wp.Auth != nil && !wp.Auth.Enabled() {
162 log.Info("browser login disabled", "reason", wp.Auth.Disabled)
f920a9f1
Keenan Nemetz
15d ago
163 } else if cfg.OpenRegistration {
ed03b3d1
Keenan Nemetz
2h ago
164 log.Info("open registration enabled", "url", cfg.BaseURL()+"/-/register")
6c84d387
Keenan Nemetz
15d ago
165 }
653e0cfc
Keenan Nemetz
17d ago
166
a5f19b3c
Keenan Nemetz
17d ago
167 s := &Server{
6c84d387
Keenan Nemetz
15d ago
168 Cfg: cfg, Repos: inst.Repos, Identity: inst.Identity, Sessions: inst.Sessions,
642e5994
Keenan Nemetz
14d ago
169 Runs: inst.Runs, Index: inst.Index, acme: acme,
0c40a1d3
Keenan Nemetz
7d ago
170 LFS: inst.LFS, Locks: inst.Locks, PageViews: inst.PageViews, Secrets: inst.Secrets,
4506a4d3
Keenan Nemetz
13d ago
171 Proxy: proxy, SSH: ssh, Web: wp, Log: log,
eb07ff56
Keenan Nemetz
8d ago
172 metadataWake: make(chan struct{}, 1),
6ce635a8
Keenan Nemetz
1d ago
173 InstanceRepo: inst.InstanceRepo,
a5f19b3c
Keenan Nemetz
17d ago
174 }
5ece70a5
Keenan Nemetz
10d ago
175 wp.Logs = inst.Logs
6c84d387
Keenan Nemetz
15d ago
176 wp.AllowCI = s.allowContributorCI
e5478085
Keenan Nemetz
11d ago
177 wp.EnsureCandidate = s.ensureCandidateForWeb
eb07ff56
Keenan Nemetz
8d ago
178 wp.MetadataChanged = s.wakeMetadata
179 wp.DispatchWorkflow = s.dispatchWorkflow
180 ssh.SetDispatchWorkflow(s.dispatchWorkflow)
2973f727
Keenan Nemetz
15d ago
181 wp.RenameRepo = inst.RenameRepo
182 wp.DeleteRepo = inst.DeleteRepo
d3296cda
Keenan Nemetz
17d ago
183 // Sites are opt-in: without a pages domain there is no host to serve them on,
184 // and an instance that never wanted them pays no processes for the idea.
185 if cfg.PagesDomain != "" {
186 s.Pages = pages.New(pages.Config{
187 Domain: cfg.PagesDomain,
188 Ref: cfg.PagesRef,
189 Procs: cfg.PagesProcs,
190 CacheBytes: cfg.PagesCacheBytes,
191 Log: log,
c5902451
Keenan Nemetz
16d ago
192 }, inst.Repos)
d3296cda
Keenan Nemetz
17d ago
193 }
653e0cfc
Keenan Nemetz
17d ago
194 s.mux = s.routes()
b44cbfaf
Keenan Nemetz
17d ago
195 // Background loops start here rather than in Run: a caller that serves the
196 // listeners itself (the tests, an embedder) must still get roster reloads,
197 // queue ingestion and lease expiry.
198 s.ctx, s.cancel = context.WithCancel(context.Background())
6ce635a8
Keenan Nemetz
1d ago
199 if s.InstanceRepo != nil && s.InstanceRepo.Enabled() {
200 if err := s.InstanceRepo.Reload(); err != nil {
201 log.Warn("instance repository: cannot load", "repo", cfg.InstanceRepo, "err", err)
202 }
203 go s.watchInstanceRepo(s.ctx)
204 }
b44cbfaf
Keenan Nemetz
17d ago
205 go s.watchRoster(s.ctx)
206 go s.watchSpool(s.ctx)
a945ae55
Keenan Nemetz
16d ago
207 go s.watchRefs(s.ctx)
eb07ff56
Keenan Nemetz
8d ago
208 go s.watchMetadata(s.ctx)
209 go s.watchSchedules(s.ctx)
e5478085
Keenan Nemetz
11d ago
210 go s.watchCandidates(s.ctx)
211 go s.watchAutoMerges(s.ctx)
b44cbfaf
Keenan Nemetz
17d ago
212 go s.watchLeases(s.ctx)
65b88b3b
Keenan Nemetz
16d ago
213 // An instance that stored objects before they were scoped by repository needs its
214 // links, and nobody reads an upgrade note. This is idempotent and deletes nothing.
215 s.scopeLFSStore()
653e0cfc
Keenan Nemetz
17d ago
216 return s, nil
217 }
218
d3296cda
Keenan Nemetz
17d ago
219 // Handler returns the HTTP surface. Site hosts are dispatched first: a request
220 // for <repo>.<pages_domain> is a page, and everything else is the forge UI.
a76b2f38
Keenan Nemetz
12d ago
221 //
222 // Sites are compressed here rather than inside the pages package because the
223 // dispatcher is the last point both surfaces pass through, and a site is a static
224 // file server: HTML, CSS and JavaScript, which is exactly what gzip is for. The
225 // forge UI does its own, inside its middleware, because the compressor has to sit
226 // under the status recorder there.
d3296cda
Keenan Nemetz
17d ago
227 func (s *Server) Handler() http.Handler {
ed03b3d1
Keenan Nemetz
2h ago
228 forge := s.Web.Middleware(s.forgeHandler(s.mux))
d3296cda
Keenan Nemetz
17d ago
229 if s.Pages == nil {
6c84d387
Keenan Nemetz
15d ago
230 return forge
d3296cda
Keenan Nemetz
17d ago
231 }
a76b2f38
Keenan Nemetz
12d ago
232 sites := compress.Handler(s.Pages)
d3296cda
Keenan Nemetz
17d ago
233 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
234 if _, ok := s.Pages.Host(r.Host); ok {
a76b2f38
Keenan Nemetz
12d ago
235 sites.ServeHTTP(w, r)
d3296cda
Keenan Nemetz
17d ago
236 return
237 }
6c84d387
Keenan Nemetz
15d ago
238 forge.ServeHTTP(w, r)
d3296cda
Keenan Nemetz
17d ago
239 })
240 }
653e0cfc
Keenan Nemetz
17d ago
241
73755a56
Keenan Nemetz
9d ago
242 // drainGrace is how long a clean shutdown lets an already-leased job finish before
243 // closing the listeners. It is shorter than systemd's default stop timeout, which
244 // leaves room for the HTTP server's own shutdown. Work that does not fit in the
245 // window is still lost on a restart; a result that arrives inside it is not.
246 const drainGrace = time.Minute
247
248 // drain stops new leases and waits for the jobs already running to report. It is
249 // best effort: a runner that died, or a build longer than the grace, is left for
250 // the next process, which can requeue it from the durable lease.
251 func (s *Server) drain() {
252 if s.Runs == nil {
253 return
254 }
255 s.Runs.Drain()
256 n, err := s.Runs.Running()
257 if err != nil {
258 s.Log.Warn("drain: cannot count running jobs", "err", err)
259 return
260 }
261 if n == 0 {
262 return
263 }
264 s.Log.Info("draining running jobs", "jobs", n, "grace", drainGrace)
265 deadline := time.Now().Add(drainGrace)
266 for time.Now().Before(deadline) {
267 time.Sleep(250 * time.Millisecond)
268 n, err = s.Runs.Running()
269 if err != nil {
270 s.Log.Warn("drain: cannot count running jobs", "err", err)
271 return
272 }
273 if n == 0 {
274 s.Log.Info("running jobs finished; shutting down")
275 return
276 }
277 }
278 s.Log.Warn("drain grace expired with jobs still running", "jobs", n)
279 }
280
642e5994
Keenan Nemetz
14d ago
281 // Run starts the listeners and blocks until ctx is cancelled. With automatic
282 // HTTPS, the HTTP address only answers ACME challenges and redirects, and the
283 // forge is served over TLS on HTTPSAddr.
a5f19b3c
Keenan Nemetz
17d ago
284 func (s *Server) Run(ctx context.Context) error {
285 sshLn, err := net.Listen("tcp", s.Cfg.SSHAddr)
286 if err != nil {
287 return fmt.Errorf("ssh listen %s: %w", s.Cfg.SSHAddr, err)
288 }
289 httpLn, err := net.Listen("tcp", s.Cfg.HTTPAddr)
290 if err != nil {
291 sshLn.Close()
292 return fmt.Errorf("http listen %s: %w", s.Cfg.HTTPAddr, err)
293 }
294
642e5994
Keenan Nemetz
14d ago
295 httpSrv := &http.Server{
a5f19b3c
Keenan Nemetz
17d ago
296 Handler: s.Handler(),
297 ReadHeaderTimeout: 15 * time.Second,
86991e70
Keenan Nemetz
13d ago
298 // ReadTimeout bounds the request, body included. The only bodies on the
299 // HTTP surface are a login form, a comment and a fetch negotiation, all of
300 // which are small and arrive long before this.
301 ReadTimeout: 60 * time.Second,
302 // WriteTimeout is deliberately unset: a clone, a release tarball, an LFS
303 // object and a site file are streamed, and a wall-clock limit on writing the
304 // response would truncate a legitimate large download. Those streams are
305 // bounded where they are produced instead — the git transport by its own
306 // request deadline, archives by a concurrency bound, downloads by a size cap.
307 MaxHeaderBytes: 64 << 10,
308 IdleTimeout: 120 * time.Second,
a5f19b3c
Keenan Nemetz
17d ago
309 }
642e5994
Keenan Nemetz
14d ago
310 var httpsLn net.Listener
311 var httpsSrv *http.Server
312 if s.acme != nil {
313 httpSrv.Handler = s.acme.httpHandler()
314 httpsLn, err = net.Listen("tcp", s.Cfg.HTTPSAddr)
315 if err != nil {
316 httpLn.Close()
317 sshLn.Close()
318 return fmt.Errorf("https listen %s: %w", s.Cfg.HTTPSAddr, err)
319 }
320 httpsSrv = &http.Server{
321 Handler: s.Handler(),
322 TLSConfig: s.acme.tlsConfig(),
323 ReadHeaderTimeout: 15 * time.Second,
86991e70
Keenan Nemetz
13d ago
324 ReadTimeout: 60 * time.Second,
325 MaxHeaderBytes: 64 << 10,
642e5994
Keenan Nemetz
14d ago
326 IdleTimeout: 120 * time.Second,
327 }
328 }
329
330 errCh := make(chan error, 3)
a5f19b3c
Keenan Nemetz
17d ago
331 go func() {
332 if err := s.SSH.Serve(sshLn); err != nil {
333 errCh <- fmt.Errorf("ssh serve: %w", err)
653e0cfc
Keenan Nemetz
17d ago
334 }
a5f19b3c
Keenan Nemetz
17d ago
335 }()
336 go func() {
642e5994
Keenan Nemetz
14d ago
337 if err := httpSrv.Serve(httpLn); err != nil && !errors.Is(err, http.ErrServerClosed) {
a5f19b3c
Keenan Nemetz
17d ago
338 errCh <- fmt.Errorf("http serve: %w", err)
339 }
340 }()
642e5994
Keenan Nemetz
14d ago
341 if httpsSrv != nil {
342 go func() {
343 if err := httpsSrv.ServeTLS(httpsLn, "", ""); err != nil && !errors.Is(err, http.ErrServerClosed) {
344 errCh <- fmt.Errorf("https serve: %w", err)
345 }
346 }()
347 }
a5f19b3c
Keenan Nemetz
17d ago
348
642e5994
Keenan Nemetz
14d ago
349 if httpsSrv != nil {
350 s.Log.Info("listening", "ssh", sshLn.Addr().String(), "http", httpLn.Addr().String(),
351 "https", httpsLn.Addr().String(), "data", s.Cfg.DataDir, "users", s.Identity.Snapshot().Count)
352 } else {
353 s.Log.Info("listening", "ssh", sshLn.Addr().String(), "http", httpLn.Addr().String(),
354 "data", s.Cfg.DataDir, "users", s.Identity.Snapshot().Count)
355 }
a5f19b3c
Keenan Nemetz
17d ago
356
357 select {
358 case <-ctx.Done():
73755a56
Keenan Nemetz
9d ago
359 // Let the runners finish what they already hold before the sockets go away.
360 // A deploy that cuts a job in its last step costs a whole second attempt.
361 s.drain()
a5f19b3c
Keenan Nemetz
17d ago
362 sh, cancel := context.WithTimeout(context.Background(), 20*time.Second)
363 defer cancel()
364 s.SSH.Close()
642e5994
Keenan Nemetz
14d ago
365 httpSrv.Shutdown(sh)
366 if httpsSrv != nil {
367 httpsSrv.Shutdown(sh)
368 }
a5f19b3c
Keenan Nemetz
17d ago
369 return nil
370 case err := <-errCh:
371 s.SSH.Close()
642e5994
Keenan Nemetz
14d ago
372 httpSrv.Close()
373 if httpsSrv != nil {
374 httpsSrv.Close()
375 }
a5f19b3c
Keenan Nemetz
17d ago
376 return err
653e0cfc
Keenan Nemetz
17d ago
377 }
a5f19b3c
Keenan Nemetz
17d ago
378 }
379
197ecaa2
Keenan Nemetz
17d ago
380 func (s *Server) watchRoster(ctx context.Context) {
381 t := time.NewTicker(5 * time.Second)
382 defer t.Stop()
383 for {
384 select {
385 case <-ctx.Done():
386 return
387 case <-t.C:
31df8bae
Keenan Nemetz
17d ago
388 before := s.Identity.Snapshot().Count
197ecaa2
Keenan Nemetz
17d ago
389 if err := s.Identity.Reload(); err != nil {
390 s.Log.Error("roster reload failed; keeping the previous snapshot", "err", err)
391 continue
392 }
31df8bae
Keenan Nemetz
17d ago
393 if after := s.Identity.Snapshot().Count; after != before {
427c4761
Keenan Nemetz
17d ago
394 s.Log.Info("roster reloaded", "accounts", after)
197ecaa2
Keenan Nemetz
17d ago
395 }
396 }
397 }
398 }
399
a5f19b3c
Keenan Nemetz
17d ago
400 func (s *Server) Close() error {
b44cbfaf
Keenan Nemetz
17d ago
401 if s.cancel != nil {
402 s.cancel()
403 }
d3296cda
Keenan Nemetz
17d ago
404 if s.Pages != nil {
405 s.Pages.Close()
406 }
b44cbfaf
Keenan Nemetz
17d ago
407 if s.Runs != nil {
408 _ = s.Runs.Close()
409 }
bf1e027c
Keenan Nemetz
16d ago
410 if s.Locks != nil {
411 _ = s.Locks.Close()
412 }
4506a4d3
Keenan Nemetz
13d ago
413 if s.PageViews != nil {
414 _ = s.PageViews.Close()
415 }
653e0cfc
Keenan Nemetz
17d ago
416 if s.Index != nil {
427c4761
Keenan Nemetz
17d ago
417 if err := s.Index.Close(); err != nil {
418 return err
419 }
420 }
6c84d387
Keenan Nemetz
15d ago
421 if s.Sessions != nil {
422 _ = s.Sessions.Close()
423 }
427c4761
Keenan Nemetz
17d ago
424 if s.Identity != nil {
425 return s.Identity.Close()
653e0cfc
Keenan Nemetz
17d ago
426 }
a5f19b3c
Keenan Nemetz
17d ago
427 return nil
653e0cfc
Keenan Nemetz
17d ago
428 }
429
430 func (s *Server) RebuildIndex() (int, error) { return s.Index.Rebuild(s.Repos) }
431
a5f19b3c
Keenan Nemetz
17d ago
432 // MaxInputLabel is used by logging to show the effective push cap.
433 func (s *Server) MaxInputLabel() string {
434 return strconv.FormatInt(s.Cfg.ReceiveMaxBytes, 10)
653e0cfc
Keenan Nemetz
17d ago
435 }