Skip to content

Commit 2f96e5a

Browse files
committed
Merge pull request #22 from joeshaw/maintenance-mode
add a Heroku-like maintenance mode
2 parents d186a04 + deee3b7 commit 2f96e5a

File tree

7 files changed

+212
-100
lines changed

7 files changed

+212
-100
lines changed

admin.go

+30-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func getStats(w http.ResponseWriter, r *http.Request) {
2626
w.Write(marshal(Registry.Stats()))
2727
}
2828

29-
func getService(w http.ResponseWriter, r *http.Request) {
29+
func getServiceStats(w http.ResponseWriter, r *http.Request) {
3030
vars := mux.Vars(r)
3131

3232
serviceStats, err := Registry.ServiceStats(vars["service"])
@@ -38,6 +38,18 @@ func getService(w http.ResponseWriter, r *http.Request) {
3838
w.Write(marshal(serviceStats))
3939
}
4040

41+
func getServiceConfig(w http.ResponseWriter, r *http.Request) {
42+
vars := mux.Vars(r)
43+
44+
serviceStats, err := Registry.ServiceConfig(vars["service"])
45+
if err != nil {
46+
http.Error(w, err.Error(), http.StatusNotFound)
47+
return
48+
}
49+
50+
w.Write(marshal(serviceStats))
51+
}
52+
4153
// Update the global config
4254
func postConfig(w http.ResponseWriter, r *http.Request) {
4355
cfg := client.Config{}
@@ -120,6 +132,20 @@ func deleteService(w http.ResponseWriter, r *http.Request) {
120132
w.Write(marshal(Registry.Config()))
121133
}
122134

135+
func getBackendStats(w http.ResponseWriter, r *http.Request) {
136+
vars := mux.Vars(r)
137+
serviceName := vars["service"]
138+
backendName := vars["backend"]
139+
140+
backend, err := Registry.BackendStats(serviceName, backendName)
141+
if err != nil {
142+
http.Error(w, err.Error(), http.StatusNotFound)
143+
return
144+
}
145+
146+
w.Write(marshal(backend))
147+
}
148+
123149
func getBackend(w http.ResponseWriter, r *http.Request) {
124150
vars := mux.Vars(r)
125151
serviceName := vars["service"]
@@ -187,7 +213,9 @@ func addHandlers() {
187213
r.HandleFunc("/_config", getConfig).Methods("GET")
188214
r.HandleFunc("/_config", postConfig).Methods("PUT", "POST")
189215
r.HandleFunc("/_stats", getStats).Methods("GET")
190-
r.HandleFunc("/{service}", getService).Methods("GET")
216+
r.HandleFunc("/{service}", getServiceStats).Methods("GET")
217+
r.HandleFunc("/{service}/_config", getServiceConfig).Methods("GET")
218+
r.HandleFunc("/{service}/_stats", getServiceStats).Methods("GET")
191219
r.HandleFunc("/{service}", postService).Methods("PUT", "POST")
192220
r.HandleFunc("/{service}", deleteService).Methods("DELETE")
193221
r.HandleFunc("/{service}/{backend}", getBackend).Methods("GET")

admin_test.go

+52
Original file line numberDiff line numberDiff line change
@@ -714,3 +714,55 @@ func (s *HTTPSuite) TestHTTPSRedirect(c *C) {
714714
}
715715
c.Assert(resp.StatusCode, Equals, http.StatusOK)
716716
}
717+
718+
func (s *HTTPSuite) TestMaintenanceMode(c *C) {
719+
mainServer := s.backendServers[0]
720+
errServer := s.backendServers[1]
721+
722+
svcCfg := client.ServiceConfig{
723+
Name: "VHostTest1",
724+
Addr: "127.0.0.1:9000",
725+
VirtualHosts: []string{"vhost1.test"},
726+
Backends: []client.BackendConfig{
727+
{Addr: mainServer.addr},
728+
},
729+
MaintenanceMode: true,
730+
}
731+
732+
if err := Registry.AddService(svcCfg); err != nil {
733+
c.Fatal(err)
734+
}
735+
736+
// No error page is registered, so we should just get a 503 error with no body
737+
checkHTTP("https://vhost1.test:"+s.httpsPort+"/addr", "vhost1.test", "", 503, c)
738+
739+
// Use another backend to provide the error page
740+
svcCfg.ErrorPages = map[string][]int{
741+
"http://" + errServer.addr + "/error?code=503": []int{503},
742+
}
743+
744+
if err := Registry.UpdateService(svcCfg); err != nil {
745+
c.Fatal(err)
746+
}
747+
748+
// Get a 503 error with the cached body
749+
checkHTTP("https://vhost1.test:"+s.httpsPort+"/addr", "vhost1.test", errServer.addr, 503, c)
750+
751+
// Turn maintenance mode off
752+
svcCfg.MaintenanceMode = false
753+
754+
if err := Registry.UpdateService(svcCfg); err != nil {
755+
c.Fatal(err)
756+
}
757+
758+
checkHTTP("https://vhost1.test:"+s.httpsPort+"/addr", "vhost1.test", mainServer.addr, 200, c)
759+
760+
// Turn it back on
761+
svcCfg.MaintenanceMode = true
762+
763+
if err := Registry.UpdateService(svcCfg); err != nil {
764+
c.Fatal(err)
765+
}
766+
767+
checkHTTP("https://vhost1.test:"+s.httpsPort+"/addr", "vhost1.test", errServer.addr, 503, c)
768+
}

client/config.go

+38-31
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,10 @@ type ServiceConfig struct {
211211

212212
// Backends is a list of all servers handling connections for this service.
213213
Backends []BackendConfig `json:"backends,omitempty"`
214+
215+
// Maintenance mode is a flag to return 503 status codes to clients
216+
// without visiting backends.
217+
MaintenanceMode bool `json:"maintenance_mode"`
214218
}
215219

216220
// Return a copy of ServiceConfig with any unset fields to their default
@@ -285,53 +289,56 @@ func (b *ServiceConfig) String() string {
285289
return string(b.Marshal())
286290
}
287291

288-
// Update any unset fields with those from the supplied config.
289-
// FIXME: HTTPSRedirect won't be turned off. Maybe change it to *bool?
290-
func (s *ServiceConfig) Merge(cfg ServiceConfig) {
292+
// Create a new config by merging the values from the current config
293+
// with those set in the new config
294+
func (s ServiceConfig) Merge(cfg ServiceConfig) ServiceConfig {
295+
new := s
296+
291297
// let's try not to change the name
292-
s.Name = cfg.Name
298+
new.Name = cfg.Name
293299

294-
if s.Addr == "" {
295-
s.Addr = cfg.Addr
300+
if cfg.Addr != "" {
301+
new.Addr = cfg.Addr
296302
}
297-
if s.Network == "" {
298-
s.Network = cfg.Network
303+
if cfg.Network != "" {
304+
new.Network = cfg.Network
299305
}
300-
if s.Balance == "" {
301-
s.Balance = cfg.Balance
306+
if cfg.Balance != "" {
307+
new.Balance = cfg.Balance
302308
}
303-
if s.CheckInterval == 0 {
304-
s.CheckInterval = cfg.CheckInterval
309+
if cfg.CheckInterval != 0 {
310+
new.CheckInterval = cfg.CheckInterval
305311
}
306-
if s.Fall == 0 {
307-
s.Fall = cfg.Fall
312+
if cfg.Fall != 0 {
313+
new.Fall = cfg.Fall
308314
}
309-
if s.Rise == 0 {
310-
s.Rise = cfg.Rise
315+
if cfg.Rise != 0 {
316+
new.Rise = cfg.Rise
311317
}
312-
if s.ClientTimeout == 0 {
313-
s.ClientTimeout = cfg.ClientTimeout
318+
if cfg.ClientTimeout != 0 {
319+
new.ClientTimeout = cfg.ClientTimeout
314320
}
315-
if s.ServerTimeout == 0 {
316-
s.ServerTimeout = cfg.ServerTimeout
321+
if cfg.ServerTimeout != 0 {
322+
new.ServerTimeout = cfg.ServerTimeout
317323
}
318-
if s.DialTimeout == 0 {
319-
s.DialTimeout = cfg.DialTimeout
324+
if cfg.DialTimeout != 0 {
325+
new.DialTimeout = cfg.DialTimeout
320326
}
321327

322-
if cfg.HTTPSRedirect {
323-
s.HTTPSRedirect = cfg.HTTPSRedirect
328+
if cfg.VirtualHosts != nil {
329+
new.VirtualHosts = cfg.VirtualHosts
324330
}
325331

326-
if s.VirtualHosts == nil {
327-
s.VirtualHosts = cfg.VirtualHosts
332+
if cfg.ErrorPages != nil {
333+
new.ErrorPages = cfg.ErrorPages
328334
}
329335

330-
if s.ErrorPages == nil {
331-
s.ErrorPages = cfg.ErrorPages
336+
if cfg.Backends != nil {
337+
new.Backends = cfg.Backends
332338
}
333339

334-
if s.Backends == nil {
335-
s.Backends = cfg.Backends
336-
}
340+
new.HTTPSRedirect = cfg.HTTPSRedirect
341+
new.MaintenanceMode = cfg.MaintenanceMode
342+
343+
return new
337344
}

http.go

+18-19
Original file line numberDiff line numberDiff line change
@@ -401,36 +401,35 @@ func (e *ErrorResponse) CheckResponse(pr *ProxyRequest) bool {
401401
return true
402402
}
403403

404+
func logRequest(req *http.Request, statusCode int, backend string, proxyError error, duration time.Duration) {
405+
id := req.Header.Get("X-Request-Id")
406+
method := req.Method
407+
url := req.Host + req.RequestURI
408+
agent := req.UserAgent()
409+
410+
clientIP := req.Header.Get("X-Forwarded-For")
411+
if clientIP == "" {
412+
clientIP = req.RemoteAddr
413+
}
414+
415+
errStr := fmt.Sprintf("%v", proxyError)
416+
fmtStr := "id=%s method=%s client-ip=%s url=%s backend=%s status=%d duration=%s agent=%s, err=%s"
417+
log.Printf(fmtStr, id, method, clientIP, url, backend, statusCode, duration, agent, errStr)
418+
}
419+
404420
func logProxyRequest(pr *ProxyRequest) bool {
405421
// TODO: we may to be able to switch this off
406422
if pr == nil || pr.Request == nil {
407423
return true
408424
}
409425

410-
var id, method, clientIP, url, backend, agent string
411-
var status int
412-
413426
duration := pr.FinishTime.Sub(pr.StartTime)
414427

415-
id = pr.Request.Header.Get("X-Request-Id")
416-
method = pr.Request.Method
417-
url = pr.Request.Host + pr.Request.RequestURI
418-
agent = pr.Request.UserAgent()
419-
status = pr.Response.StatusCode
420-
421-
clientIP = pr.Request.Header.Get("X-Forwarded-For")
422-
if clientIP == "" {
423-
clientIP = pr.Request.RemoteAddr
424-
}
425-
428+
var backend string
426429
if pr.Response != nil && pr.Response.Request != nil && pr.Response.Request.URL != nil {
427430
backend = pr.Response.Request.URL.Host
428431
}
429432

430-
err := fmt.Sprintf("%v", pr.ProxyError)
431-
432-
fmtStr := "id=%s method=%s client-ip=%s url=%s backend=%s status=%d duration=%s agent=%s, err=%s"
433-
434-
log.Printf(fmtStr, id, method, clientIP, url, backend, status, duration, agent, err)
433+
logRequest(pr.Request, pr.Response.StatusCode, backend, pr.ProxyError, duration)
435434
return true
436435
}

registry.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ func (s *ServiceRegistry) UpdateService(newCfg client.ServiceConfig) error {
293293
}
294294

295295
currentCfg := service.Config()
296-
newCfg.Merge(currentCfg)
296+
newCfg = currentCfg.Merge(newCfg)
297297

298298
if err := service.UpdateConfig(newCfg); err != nil {
299299
return err

0 commit comments

Comments
 (0)