This repository has been archived by the owner on Dec 26, 2023. It is now read-only.
/
server.go
180 lines (150 loc) · 4.42 KB
/
server.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package http
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/felixge/httpsnoop"
gorillaHandlers "github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/go-logr/logr"
"github.com/leg100/otf/internal"
"github.com/leg100/otf/internal/http/html"
"github.com/leg100/otf/internal/json"
)
const (
// shutdownTimeout is the time given for outstanding requests to finish
// before shutdown.
shutdownTimeout = 1 * time.Second
headersKey key = "headers"
)
var (
healthzPayload = json.MustMarshal(struct {
Version string
Commit string
Built string
}{
Version: internal.Version,
Commit: internal.Commit,
Built: internal.Built,
})
)
type (
// ServerConfig is the http server config
ServerConfig struct {
SSL bool
CertFile, KeyFile string
EnableRequestLogging bool
DevMode bool
Handlers []internal.Handlers
// middleware to intercept requests, executed in the order given.
Middleware []mux.MiddlewareFunc
}
// Server is the http server for OTF
Server struct {
logr.Logger
ServerConfig
server *http.Server
}
// unexported type for use with embedding values in contexts
key string
)
// NewServer constructs the http server for OTF
func NewServer(logger logr.Logger, cfg ServerConfig) (*Server, error) {
if cfg.SSL {
if cfg.CertFile == "" || cfg.KeyFile == "" {
return nil, fmt.Errorf("must provide both --cert-file and --key-file")
}
}
r := mux.NewRouter()
// Catch panics and return 500s
r.Use(gorillaHandlers.RecoveryHandler(gorillaHandlers.PrintRecoveryStack(true)))
r.Handle("/", http.RedirectHandler("/app/organizations", http.StatusFound))
// Serve static files
if err := html.AddStaticHandler(logger, r, cfg.DevMode); err != nil {
return nil, err
}
// Prometheus metrics
r.HandleFunc("/metrics", promhttp.Handler().ServeHTTP)
r.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "application/json")
w.Write(healthzPayload)
})
r.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "application/json")
w.Write([]byte(`{"status":"OK"}`))
})
// Subrouter for service routes
svcRouter := r.NewRoute().Subrouter()
// this middleware adds http headers from the request to the context
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), headersKey, r.Header)
next.ServeHTTP(w, r.WithContext(ctx))
})
})
// Subject service routes to provided middleware, verifying tokens,
// sessions.
svcRouter.Use(cfg.Middleware...)
// Add handlers for each service
for _, h := range cfg.Handlers {
h.AddHandlers(svcRouter)
}
// Optionally log every request
if cfg.EnableRequestLogging {
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m := httpsnoop.CaptureMetrics(next, w, r)
logger.Info("request",
"duration", fmt.Sprintf("%dms", m.Duration.Milliseconds()),
"status", m.Code,
"method", r.Method,
"path", fmt.Sprintf("%s?%s", r.URL.Path, r.URL.RawQuery))
})
})
}
return &Server{
Logger: logger,
ServerConfig: cfg,
server: &http.Server{Handler: r},
}, nil
}
// Start starts serving http traffic on the given listener and waits until the server exits due to
// error or the context is cancelled.
func (s *Server) Start(ctx context.Context, ln net.Listener) (err error) {
errch := make(chan error)
go func() {
if s.SSL {
errch <- s.server.ServeTLS(ln, s.CertFile, s.KeyFile)
} else {
errch <- s.server.Serve(ln)
}
}()
s.Info("started server", "address", ln.Addr().String(), "ssl", s.SSL)
// Block until server stops listening or context is cancelled.
select {
case err := <-errch:
if err == http.ErrServerClosed {
return nil
}
return err
case <-ctx.Done():
s.Info("gracefully shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := s.server.Shutdown(ctx); err != nil {
return s.server.Close()
}
return nil
}
}
func HeadersFromContext(ctx context.Context) (http.Header, error) {
headers, ok := ctx.Value(headersKey).(http.Header)
if !ok {
return nil, errors.New("no http headers found in context")
}
return headers, nil
}