aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2022-05-22 00:59:57 -0700
committerMike Crute <mike@crute.us>2022-05-22 00:59:57 -0700
commitc34185d893311e31ecfbc72d4e306fb57e56e8fa (patch)
treecfe581cb9d9bf3398adc508e7cdd47bfb576904b
parentd25729cef991e6136eede4931e3d46a76d473391 (diff)
downloadgolib-c34185d893311e31ecfbc72d4e306fb57e56e8fa.tar.bz2
golib-c34185d893311e31ecfbc72d4e306fb57e56e8fa.tar.xz
golib-c34185d893311e31ecfbc72d4e306fb57e56e8fa.zip
echo: completely decouple new/configecho/v0.7.0db/mongodb/v0.5.0
-rw-r--r--echo/echo_default.go299
1 files changed, 166 insertions, 133 deletions
diff --git a/echo/echo_default.go b/echo/echo_default.go
index 6977eed..dcdda63 100644
--- a/echo/echo_default.go
+++ b/echo/echo_default.go
@@ -7,15 +7,13 @@ import (
7 "html/template" 7 "html/template"
8 "io/fs" 8 "io/fs"
9 "net/http" 9 "net/http"
10 "os"
11 "sync" 10 "sync"
12 11
13 "code.crute.us/mcrute/golib/clients/netbox" 12 "code.crute.us/mcrute/golib/clients/netbox"
14 glautocert "code.crute.us/mcrute/golib/crypto/acme/autocert" 13 "code.crute.us/mcrute/golib/crypto/acme/autocert"
15 glmw "code.crute.us/mcrute/golib/echo/middleware" 14 glmw "code.crute.us/mcrute/golib/echo/middleware"
16 "code.crute.us/mcrute/golib/echo/prometheus" 15 "code.crute.us/mcrute/golib/echo/prometheus"
17 "code.crute.us/mcrute/golib/service" 16 "code.crute.us/mcrute/golib/service"
18 glservice "code.crute.us/mcrute/golib/service"
19 17
20 "github.com/labstack/echo/v4" 18 "github.com/labstack/echo/v4"
21 "github.com/labstack/echo/v4/middleware" 19 "github.com/labstack/echo/v4/middleware"
@@ -54,13 +52,18 @@ func makeMiddlewareSkipper(c *prometheus.PrometheusConfig) middleware.Skipper {
54 } 52 }
55} 53}
56 54
55// EchoConfig is the configuration for an EchoWrapper
56//
57// Service clients are generally passed in an only by interface even
58// though we could create these internally so as to provide some
59// isolation between what is (theoretically) a re-usable, though
60// opinionated, Echo instance vs something that is completely custom to
61// internal use-cases.
57type EchoConfig struct { 62type EchoConfig struct {
58 Autocert glautocert.PrimingCertProvider 63 Autocert autocert.PrimingCertProvider
59 NetboxClient netbox.NetboxClient 64 NetboxClient netbox.NetboxClient
60 ApplicationName string 65 ApplicationName string
61 ApplicationVersion string 66 ApplicationVersion string
62 DisableServerHeader bool
63 Debug bool
64 BindAddresses []string 67 BindAddresses []string
65 BodySizeLimit string 68 BodySizeLimit string
66 TrustedProxyIPRanges []string 69 TrustedProxyIPRanges []string
@@ -77,12 +80,36 @@ type EchoConfig struct {
77 80
78type EchoWrapper struct { 81type EchoWrapper struct {
79 *echo.Echo 82 *echo.Echo
80 Autocert glautocert.PrimingCertProvider 83 runner *service.AppRunner
81 middlewareJobs []glservice.RunnerFunc 84 autocert autocert.PrimingCertProvider
82 middlewareInitJobs []glservice.SyncRunnerFunc 85 templateFS fs.FS
83 servers []*http.Server 86}
84 tlsServers []*http.Server 87
85 templateFS fs.FS 88// NewEchoWrapper creates a new instance of Echo and wraps it in an
89// internal wrapper that does configuration and service running.
90//
91// It is expected that Configure is called before using this Echo
92// instance.
93func NewEchoWrapper(ctx context.Context, debug bool) (*EchoWrapper, error) {
94 e := echo.New()
95 e.Debug = debug
96
97 e.Logger.SetLevel(log.INFO)
98 if debug {
99 e.Logger.SetLevel(log.DEBUG)
100 }
101
102 e.Use(middleware.Logger())
103 e.Use(glmw.Recover())
104
105 return &EchoWrapper{
106 Echo: e,
107 runner: service.NewAppRunner(ctx, e.Logger),
108 }, nil
109}
110
111func (w *EchoWrapper) Runner() *service.AppRunner {
112 return w.runner
86} 113}
87 114
88func (w *EchoWrapper) CachedStaticRoute(prefix, path string) { 115func (w *EchoWrapper) CachedStaticRoute(prefix, path string) {
@@ -92,214 +119,230 @@ func (w *EchoWrapper) CachedStaticRoute(prefix, path string) {
92 StaticFS(w.GET, w.templateFS, prefix, path, glmw.CacheOneMonthMiddleware) 119 StaticFS(w.GET, w.templateFS, prefix, path, glmw.CacheOneMonthMiddleware)
93} 120}
94 121
95func (w *EchoWrapper) makeServerJob(s *http.Server, echoInit bool) glservice.RunnerFunc { 122func (w *EchoWrapper) RunForever(enableBackgroundJobs bool) {
96 return func(ctx context.Context, wg *sync.WaitGroup) error { 123 w.runner.RunForever(enableBackgroundJobs)
97 wg.Add(1) 124}
98 defer wg.Done()
99 125
100 w.Logger.Infof("Starting server with address: %s", s.Addr) 126func (w *EchoWrapper) GetTemplateFS() fs.FS {
127 return w.templateFS
128}
101 129
102 err := make(chan error) 130func (w *EchoWrapper) Configure(c EchoConfig) error {
103 go func() { 131 w.configureAutocert(&c)
104 if s.TLSConfig == nil && echoInit {
105 err <- w.Echo.StartServer(s)
106 } else if s.TLSConfig == nil && !echoInit {
107 err <- s.ListenAndServe()
108 } else {
109 err <- s.ListenAndServeTLS("", "")
110 }
111 }()
112 select {
113 case e := <-err:
114 return e
115 default:
116 }
117 132
118 select { 133 if err := w.configureIpExtractor(&c); err != nil {
119 case <-ctx.Done(): 134 return err
120 w.Logger.Info("Shutting down web server")
121 return s.Shutdown(ctx)
122 }
123 } 135 }
124}
125 136
126func (w *EchoWrapper) makeServerJobs() []glservice.RunnerFunc { 137 if err := w.configureTemplates(&c); err != nil {
127 out := []glservice.RunnerFunc{} 138 return err
139 }
128 140
129 for i, s := range w.servers { 141 if err := w.configureCombinedLogging(&c); err != nil {
130 // The first http (not https) server should do an echo.StartServer to 142 return err
131 // configure some internal echo state and print the banner (if
132 // configured).
133 out = append(out, w.makeServerJob(s, i == 0))
134 } 143 }
135 144
136 for _, s := range w.tlsServers { 145 bindings, err := ParseAddressPortBindings(c.BindAddresses)
137 out = append(out, w.makeServerJob(s, false)) 146 if err != nil {
147 return fmt.Errorf("Error parsing address/port bindings")
138 } 148 }
139 149
140 return out 150 w.buildServers(&c, bindings)
141} 151 w.configureBodyLimit(&c)
152 w.configureRedirects(&c, bindings)
153 w.configureCompression(&c)
142 154
143func (w *EchoWrapper) AddJobsToRunner(r *service.AppRunner) { 155 w.Use(glmw.StrictSecure())
144 r.AddInitJob(w.Autocert.PrimeCache)
145 r.AddJob(w.Autocert.PrimingReporter(w.Logger))
146 156
147 r.AddJobs(w.makeServerJobs()) 157 w.configureCORS(&c)
158 w.configureCSP(&c)
159 w.configureServerHeader(&c)
160 w.configurePrometheus(&c)
148 161
149 r.AddInitJobs(w.middlewareInitJobs) 162 return nil
150 r.AddJobs(w.middlewareJobs)
151} 163}
152 164
153func (w *EchoWrapper) GetTemplateFS() fs.FS { 165func (w *EchoWrapper) configureAutocert(c *EchoConfig) {
154 return w.templateFS 166 w.autocert = c.Autocert
167 w.runner.AddInitJob(w.autocert.PrimeCache)
168 w.runner.AddJob(w.autocert.PrimingReporter(w.Logger))
155} 169}
156 170
157// NewDefaultEchoWithConfig builds a wrapper around an Echo instance and 171func (w *EchoWrapper) configureIpExtractor(c *EchoConfig) error {
158// configures it in the default way that it should probably be configured in
159// all cases. The struct returned from this function can be treated like a
160// normal Echo instance (because, for the most part it is).
161func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
162 var err error 172 var err error
163
164 mwjobs := []glservice.RunnerFunc{}
165 mwinitjobs := []glservice.SyncRunnerFunc{}
166
167 if os.Getenv("POMONA_DC_SITE") == "" {
168 return nil, fmt.Errorf("POMONA_DC_SITE must be in the environment")
169 }
170
171 e := echo.New()
172 e.Debug = c.Debug
173
174 // This is only required if the app is behind a proxy, for apps that 173 // This is only required if the app is behind a proxy, for apps that
175 // take traffic directly from the internet the IP should just be 174 // take traffic directly from the internet the IP should just be
176 // extracted from the requests RemoteAddr. 175 // extracted from the requests RemoteAddr.
177 if c.TrustedProxyIPRanges == nil { 176 if c.TrustedProxyIPRanges == nil {
178 e.IPExtractor, err = glmw.ExtractIPFromXFFHeaders(false, c.TrustedProxyIPRanges) 177 w.IPExtractor, err = glmw.ExtractIPFromXFFHeaders(false, c.TrustedProxyIPRanges)
179 if err != nil { 178 if err != nil {
180 return nil, fmt.Errorf("Error building XFF IP extractor: %w", err) 179 return fmt.Errorf("Error building XFF IP extractor: %w", err)
181 } 180 }
182 } else { 181 } else {
183 e.IPExtractor = echo.ExtractIPDirect() 182 w.IPExtractor = echo.ExtractIPDirect()
184 } 183 }
184 return nil
185}
185 186
187func (w *EchoWrapper) configureTemplates(c *EchoConfig) error {
186 // Use templates from disk in debug mode and the embedded ones that are 188 // Use templates from disk in debug mode and the embedded ones that are
187 // built-in to the binary for prod mode. 189 // built-in to the binary for prod mode.
188 var templates fs.FS 190 var templates fs.FS
189 if c.DiskTemplates != nil && c.Debug { // Debug Mode 191
192 if c.DiskTemplates != nil && w.Debug { // Debug Mode
190 templates = c.DiskTemplates 193 templates = c.DiskTemplates
191 } else if c.EmbeddedTemplates != nil && !c.Debug { // Prod Mode 194 } else if c.EmbeddedTemplates != nil && !w.Debug { // Prod Mode
192 templates = c.EmbeddedTemplates 195 templates = c.EmbeddedTemplates
193 } 196 }
194 197
195 // Only install template handlers if the path and glob are set 198 // Only install template handlers if the path and glob are set
196 if templates != nil { 199 if templates != nil {
197 e.HTTPErrorHandler = ErrorHandler(templates, c.TemplateFunctions) 200 w.HTTPErrorHandler = ErrorHandler(templates, c.TemplateFunctions)
198 201
199 tr, err := NewTemplateRenderer(templates, "*.tpl", c.TemplateFunctions) 202 tr, err := NewTemplateRenderer(templates, "*.tpl", c.TemplateFunctions)
200 if err != nil { 203 if err != nil {
201 return nil, fmt.Errorf("Error loading template renderer: %w", err) 204 return fmt.Errorf("Error loading template renderer: %w", err)
202 } 205 }
203 e.Renderer = tr 206 w.Renderer = tr
204 207
205 for _, t := range mandatoryTemplates { 208 for _, t := range mandatoryTemplates {
206 if !tr.HaveTemplate(e.NewContext(nil, nil), t) { 209 if !tr.HaveTemplate(w.NewContext(nil, nil), t) {
207 return nil, fmt.Errorf("Tempalate renderer is missing required template %s", t) 210 return fmt.Errorf("Tempalate renderer is missing required template %s", t)
208 } 211 }
209 } 212 }
210 } 213 }
211 214
212 e.Logger.SetLevel(log.INFO) 215 w.templateFS = templates
213 if c.Debug { 216 return nil
214 e.Logger.SetLevel(log.DEBUG) 217}
215 }
216 218
219func (w *EchoWrapper) configureCombinedLogging(c *EchoConfig) error {
217 if c.CombinedHostLogFile != "" { 220 if c.CombinedHostLogFile != "" {
218 lc, err := NginxCombinedHostConfigToFile(c.CombinedHostLogFile) 221 lc, err := NginxCombinedHostConfigToFile(c.CombinedHostLogFile)
219 if err != nil { 222 if err != nil {
220 return nil, fmt.Errorf("Error opening log file: %w", err) 223 return fmt.Errorf("Error opening log file: %w", err)
221 } 224 }
222 e.Use(middleware.LoggerWithConfig(lc)) 225 w.Use(middleware.LoggerWithConfig(lc))
223 } 226 }
227 return nil
228}
224 229
225 bindings, err := ParseAddressPortBindings(c.BindAddresses) 230func (w *EchoWrapper) makeServerJob(s *http.Server, echoInit bool) service.RunnerFunc {
226 if err != nil { 231 return func(ctx context.Context, wg *sync.WaitGroup) error {
227 return nil, fmt.Errorf("Error parsing address/port bindings") 232 wg.Add(1)
233 defer wg.Done()
234
235 w.Logger.Infof("Starting server with address: %s", s.Addr)
236
237 err := make(chan error)
238 go func() {
239 if s.TLSConfig == nil && echoInit {
240 err <- w.Echo.StartServer(s)
241 } else if s.TLSConfig == nil && !echoInit {
242 err <- s.ListenAndServe()
243 } else {
244 err <- s.ListenAndServeTLS("", "")
245 }
246 }()
247 select {
248 case e := <-err:
249 return e
250 default:
251 }
252
253 select {
254 case <-ctx.Done():
255 w.Logger.Info("Shutting down web server")
256 return s.Shutdown(ctx)
257 }
228 } 258 }
259}
229 260
230 servers := make([]*http.Server, len(c.BindAddresses)) 261func (w *EchoWrapper) buildServers(c *EchoConfig, bindings *AddressPortConfig) {
231 for i, a := range bindings.HttpBindings() { 262 for i, a := range bindings.HttpBindings() {
232 servers[i] = &http.Server{ 263 s := &http.Server{
233 Addr: a, 264 Addr: a,
234 Handler: e, 265 Handler: w,
235 } 266 }
267 w.runner.AddJob(w.makeServerJob(s, i == 0))
236 } 268 }
237 269
238 tlsServers := make([]*http.Server, len(c.BindAddresses)) 270 for _, a := range bindings.TlsBindings() {
239 for i, a := range bindings.TlsBindings() { 271 s := &http.Server{
240 tlsServers[i] = &http.Server{
241 Addr: a, 272 Addr: a,
242 TLSConfig: &tls.Config{ 273 TLSConfig: &tls.Config{
243 MinVersion: tls.VersionTLS12, 274 MinVersion: tls.VersionTLS12,
244 GetCertificate: c.Autocert.GetCertificate, 275 GetCertificate: c.Autocert.GetCertificate,
245 NextProtos: []string{"h2", "http/1.1"}, // enable HTTP/2 276 NextProtos: []string{"h2", "http/1.1"}, // enable HTTP/2
246 }, 277 },
247 Handler: e, 278 Handler: w,
248 } 279 }
280 w.runner.AddJob(w.makeServerJob(s, false))
249 } 281 }
282}
250 283
251 metricsSkipper := makeMiddlewareSkipper(c.PrometheusConfig) 284func (w *EchoWrapper) configureBodyLimit(c *EchoConfig) {
252
253 e.Use(middleware.Logger())
254 e.Use(glmw.Recover())
255
256 if c.BodySizeLimit == "" { 285 if c.BodySizeLimit == "" {
257 e.Use(middleware.BodyLimit(defaultBodySizeLimit)) 286 w.Use(middleware.BodyLimit(defaultBodySizeLimit))
258 } else if c.BodySizeLimit != "0" { 287 } else if c.BodySizeLimit != "0" {
259 e.Use(middleware.BodyLimit(c.BodySizeLimit)) 288 w.Use(middleware.BodyLimit(c.BodySizeLimit))
260 } 289 }
290}
291
292func (w *EchoWrapper) configureRedirects(c *EchoConfig, bindings *AddressPortConfig) {
293 metricsSkipper := makeMiddlewareSkipper(c.PrometheusConfig)
261 294
262 e.Use(glmw.HTTPSRedirectWithConfig(glmw.HTTPSRedirectConfig{ 295 w.Use(glmw.HTTPSRedirectWithConfig(glmw.HTTPSRedirectConfig{
263 Skipper: metricsSkipper, 296 Skipper: metricsSkipper,
264 Port: bindings.TlsPort, 297 Port: bindings.TlsPort,
265 })) 298 }))
266 299
267 if c.RedirectToWWW { 300 if c.RedirectToWWW {
268 e.Use(middleware.WWWRedirectWithConfig(middleware.RedirectConfig{ 301 w.Use(middleware.WWWRedirectWithConfig(middleware.RedirectConfig{
269 Skipper: metricsSkipper, 302 Skipper: metricsSkipper,
270 })) 303 }))
271 } 304 }
305}
272 306
273 e.Use(middleware.Decompress()) 307func (w *EchoWrapper) configureCompression(c *EchoConfig) {
308 metricsSkipper := makeMiddlewareSkipper(c.PrometheusConfig)
309
310 w.Use(middleware.Decompress())
274 311
275 // TODO: This mangles responses but only for Accept: */* (browsers). Why? 312 // TODO: This mangles responses but only for Accept: */* (browsers). Why?
276 e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ 313 w.Use(middleware.GzipWithConfig(middleware.GzipConfig{
277 Skipper: metricsSkipper, 314 Skipper: metricsSkipper,
278 Level: 5, 315 Level: 5,
279 })) 316 }))
317}
280 318
281 e.Use(glmw.StrictSecure()) 319func (w *EchoWrapper) configureCORS(c *EchoConfig) {
282
283 if c.CORSConfig != nil { 320 if c.CORSConfig != nil {
284 e.Use(middleware.CORSWithConfig(*c.CORSConfig)) 321 w.Use(middleware.CORSWithConfig(*c.CORSConfig))
285 } else { 322 } else {
286 e.Use(middleware.CORS()) 323 w.Use(middleware.CORS())
287 } 324 }
325}
288 326
327func (w *EchoWrapper) configureCSP(c *EchoConfig) {
289 if c.ContentSecurityPolicy != nil { 328 if c.ContentSecurityPolicy != nil {
290 e.Use(glmw.ContentSecurityPolicyWithConfig(*c.ContentSecurityPolicy)) 329 w.Use(glmw.ContentSecurityPolicyWithConfig(*c.ContentSecurityPolicy))
291 } else { 330 } else {
292 e.Use(glmw.ContentSecurityPolicyWithConfig(glmw.ContentSecurityPolicyConfig{ 331 w.Use(glmw.ContentSecurityPolicyWithConfig(glmw.ContentSecurityPolicyConfig{
293 DefaultSrc: []glmw.CSPDirective{ 332 DefaultSrc: []glmw.CSPDirective{
294 glmw.CSPSelf, 333 glmw.CSPSelf,
295 }, 334 },
296 })) 335 }))
297 } 336 }
337}
298 338
299 if c.ApplicationName != "" && c.ApplicationVersion != "" && !c.DisableServerHeader { 339func (w *EchoWrapper) configureServerHeader(c *EchoConfig) {
300 e.Use(glmw.AddServerHeader(c.ApplicationName, c.ApplicationVersion)) 340 if c.ApplicationName != "" && c.ApplicationVersion != "" {
341 w.Use(glmw.AddServerHeader(c.ApplicationName, c.ApplicationVersion))
301 } 342 }
343}
302 344
345func (w *EchoWrapper) configurePrometheus(c *EchoConfig) {
303 if !c.DisablePrometheus && c.NetboxClient != nil { 346 if !c.DisablePrometheus && c.NetboxClient != nil {
304 // TODO: Should constrain this by site probably but monitoring happens 347 // TODO: Should constrain this by site probably but monitoring happens
305 // across sites so those prefixes need to be included 348 // across sites so those prefixes need to be included
@@ -307,10 +350,10 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
307 NetboxClient: c.NetboxClient, 350 NetboxClient: c.NetboxClient,
308 Tag: "management", 351 Tag: "management",
309 IncludeLocalhost: true, 352 IncludeLocalhost: true,
310 Logger: e.Logger, 353 Logger: w.Logger,
311 } 354 }
312 mwinitjobs = append(mwinitjobs, f.Init) 355 w.runner.AddInitJob(f.Init)
313 mwjobs = append(mwjobs, f.RunRefresh) 356 w.runner.AddJob(f.RunRefresh)
314 357
315 var prom *prometheus.Prometheus 358 var prom *prometheus.Prometheus
316 if c.PrometheusConfig != nil { 359 if c.PrometheusConfig != nil {
@@ -319,17 +362,7 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
319 prom = prometheus.NewPrometheus() 362 prom = prometheus.NewPrometheus()
320 } 363 }
321 364
322 e.Use(prom.MiddlewareHandler) 365 w.Use(prom.MiddlewareHandler)
323 e.GET(prom.Config.MetricsPath, prom.MetricsHandler, f.Middleware) 366 w.GET(prom.Config.MetricsPath, prom.MetricsHandler, f.Middleware)
324 } 367 }
325
326 return &EchoWrapper{
327 Echo: e,
328 Autocert: c.Autocert,
329 servers: servers,
330 tlsServers: tlsServers,
331 templateFS: templates,
332 middlewareJobs: mwjobs,
333 middlewareInitJobs: mwinitjobs,
334 }, nil
335} 368}