v0.4.0 3027a40

server.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3// Package httpserver is the HTTP entry of ermite: the router, the security
4// headers, the request log and the graceful shutdown. Routes are added here
5// as the other adapters arrive: /-/health and the routes given in Config.
6package httpserver
7
8import (
9	"context"
10	"errors"
11	"fmt"
12	"net"
13	"net/http"
14	"os"
15	"time"
16
17	"github.com/bnema/zerowrap"
18)
19
20// ShutdownTimeout is how long Run waits for requests in flight after the
21// context ends.
22const ShutdownTimeout = 10 * time.Second
23
24// Check reports whether a dependency works. A nil result means healthy.
25type Check func(ctx context.Context) error
26
27// Route mounts a handler on the router.
28type Route struct {
29	// Pattern is a net/http.ServeMux pattern, such as "GET /{repo}/info/refs".
30	// It must not overlap the /-/ routes of the server.
31	Pattern string
32	Handler http.Handler
33}
34
35// Config configures a Server.
36type Config struct {
37	// Addr is the listen address, "host:port".
38	Addr string
39	// HealthChecks run on every GET /-/health; one failure answers 503.
40	HealthChecks []Check
41	// Routes are added to the router next to /-/health.
42	Routes []Route
43	// API serves the ConnectRPC API under /-/api/ (the prefix is stripped, so
44	// it sees "/ermite.RepoService/ListRepos"). Nil serves none.
45	API http.Handler
46	// OnShutdown functions run, each in its own goroutine, when the graceful
47	// shutdown begins. Use them to end long-lived responses such as event
48	// streams, which the shutdown would otherwise wait for.
49	OnShutdown []func()
50}
51
52// APIPrefix is where the API is mounted.
53const APIPrefix = "/-/api"
54
55// Server is the HTTP server of ermite.
56type Server struct {
57	addr       string
58	handler    http.Handler
59	onShutdown []func()
60}
61
62// New builds the server and its routes.
63func New(cfg Config) *Server {
64	mux := http.NewServeMux()
65	// "GET" patterns also match HEAD; other methods get 405 with Allow, and
66	// unknown paths get 404.
67	mux.HandleFunc("GET /-/health", healthHandler(cfg.HealthChecks))
68	if cfg.API != nil {
69		mux.Handle(APIPrefix+"/", http.StripPrefix(APIPrefix, cfg.API))
70	}
71	for _, r := range cfg.Routes {
72		mux.Handle(r.Pattern, r.Handler)
73	}
74	return &Server{addr: cfg.Addr, handler: withRequestLog(withSecurityHeaders(mux)), onShutdown: cfg.OnShutdown}
75}
76
77// Handler returns the root handler, for tests.
78func (s *Server) Handler() http.Handler { return s.handler }
79
80// Run listens on the configured address and serves until ctx ends, then
81// shuts down gracefully within ShutdownTimeout. It returns nil after a clean
82// shutdown. The logger of ctx is used for the server and its requests.
83func (s *Server) Run(ctx context.Context) error {
84	ln, err := net.Listen("tcp", s.addr)
85	if err != nil {
86		return fmt.Errorf("listen on %s: %w", s.addr, err)
87	}
88	return s.Serve(ctx, ln)
89}
90
91// Serve is Run on an existing listener, which it closes.
92func (s *Server) Serve(ctx context.Context, ln net.Listener) error {
93	log := zerowrap.FromCtx(ctx)
94	srv := &http.Server{
95		Handler: s.handler,
96		// Requests keep the logger of ctx but outlive its cancellation, so
97		// that a graceful shutdown lets them finish.
98		BaseContext:       func(net.Listener) context.Context { return context.WithoutCancel(ctx) },
99		ReadHeaderTimeout: 10 * time.Second,
100		IdleTimeout:       2 * time.Minute,
101		// No read or write timeout: Git clones and pushes can be long.
102	}
103
104	for _, f := range s.onShutdown {
105		srv.RegisterOnShutdown(f)
106	}
107
108	errc := make(chan error, 1)
109	go func() { errc <- srv.Serve(ln) }()
110	log.Info().Str("addr", ln.Addr().String()).Msg("listening")
111
112	select {
113	case err := <-errc:
114		// Serve returns before Shutdown only on a failure.
115		return fmt.Errorf("serve: %w", err)
116	case <-ctx.Done():
117	}
118
119	log.Info().Dur("timeout", ShutdownTimeout).Msg("shutting down")
120	sctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), ShutdownTimeout)
121	defer cancel()
122	if err := srv.Shutdown(sctx); err != nil {
123		_ = srv.Close()
124		return fmt.Errorf("shutdown: %w", err)
125	}
126	if err := <-errc; err != nil && !errors.Is(err, http.ErrServerClosed) {
127		return fmt.Errorf("serve: %w", err)
128	}
129	log.Info().Msg("http server stopped")
130	return nil
131}
132
133// DirWritable returns a Check that creates and removes a file in dir.
134func DirWritable(dir string) Check {
135	return func(context.Context) error {
136		f, err := os.CreateTemp(dir, ".health-*")
137		if err != nil {
138			return fmt.Errorf("data directory is not writable: %w", err)
139		}
140		name := f.Name()
141		cerr := f.Close()
142		rerr := os.Remove(name)
143		if cerr != nil {
144			return fmt.Errorf("data directory is not writable: %w", cerr)
145		}
146		if rerr != nil {
147			return fmt.Errorf("data directory is not writable: %w", rerr)
148		}
149		return nil
150	}
151}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 44

↑↓ moveenter openesc close