From a7c2412c3b86df83f6cb47432d4850c4ecef3f6a Mon Sep 17 00:00:00 2001 From: Tino Date: Sun, 3 Dec 2017 18:36:16 +0100 Subject: [PATCH] Implement custom noroute html response PR for #56 --- config/config.go | 1 + config/default.go | 23 ++++++++++---------- config/load.go | 1 + fabio.properties | 8 +++++++ main.go | 17 +++++++++++++++ proxy/http_proxy.go | 5 +++++ registry/backend.go | 4 ++++ registry/consul/backend.go | 8 +++++++ registry/static/backend.go | 19 +++++++++++++++- route/no_route.go | 44 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 route/no_route.go diff --git a/config/config.go b/config/config.go index 135a80b8a..ad99bc884 100644 --- a/config/config.go +++ b/config/config.go @@ -120,6 +120,7 @@ type Consul struct { Scheme string Token string KVPath string + NoRouteHTMLPath string TagPrefix string Register bool ServiceAddr string diff --git a/config/default.go b/config/default.go index 044c470e3..c01b22914 100644 --- a/config/default.go +++ b/config/default.go @@ -47,17 +47,18 @@ var defaultConfig = &Config{ Registry: Registry{ Backend: "consul", Consul: Consul{ - Addr: "localhost:8500", - Scheme: "http", - KVPath: "/fabio/config", - TagPrefix: "urlprefix-", - Register: true, - ServiceAddr: ":9998", - ServiceName: "fabio", - ServiceStatus: []string{"passing"}, - CheckInterval: time.Second, - CheckTimeout: 3 * time.Second, - CheckScheme: "http", + Addr: "localhost:8500", + Scheme: "http", + KVPath: "/fabio/config", + NoRouteHTMLPath: "/fabio/noroute.html", + TagPrefix: "urlprefix-", + Register: true, + ServiceAddr: ":9998", + ServiceName: "fabio", + ServiceStatus: []string{"passing"}, + CheckInterval: time.Second, + CheckTimeout: 3 * time.Second, + CheckScheme: "http", }, Timeout: 10 * time.Second, Retry: 500 * time.Millisecond, diff --git a/config/load.go b/config/load.go index 54fde1a3d..692379fd7 100644 --- a/config/load.go +++ b/config/load.go @@ -160,6 +160,7 @@ func load(cmdline, environ, envprefix []string, props *properties.Properties) (c f.StringVar(&cfg.Registry.Consul.Addr, "registry.consul.addr", defaultConfig.Registry.Consul.Addr, "address of the consul agent") f.StringVar(&cfg.Registry.Consul.Token, "registry.consul.token", defaultConfig.Registry.Consul.Token, "token for consul agent") f.StringVar(&cfg.Registry.Consul.KVPath, "registry.consul.kvpath", defaultConfig.Registry.Consul.KVPath, "consul KV path for manual overrides") + f.StringVar(&cfg.Registry.Consul.NoRouteHTMLPath, "registry.consul.noroutehtmlpath", defaultConfig.Registry.Consul.NoRouteHTMLPath, "consul KV path for HTML returned when no route is found") f.StringVar(&cfg.Registry.Consul.TagPrefix, "registry.consul.tagprefix", defaultConfig.Registry.Consul.TagPrefix, "prefix for consul tags") f.BoolVar(&cfg.Registry.Consul.Register, "registry.consul.register.enabled", defaultConfig.Registry.Consul.Register, "register fabio in consul") f.StringVar(&cfg.Registry.Consul.ServiceAddr, "registry.consul.register.addr", defaultConfig.Registry.Consul.ServiceAddr, "service registration address") diff --git a/fabio.properties b/fabio.properties index 092a19639..809295115 100644 --- a/fabio.properties +++ b/fabio.properties @@ -567,6 +567,14 @@ # # registry.consul.kvpath = /fabio/config +# registry.consul.noroutehtmlpath configures the KV path for the HTML of the +# noroutes page. +# +# The consul KV path is watched for changes. +# +# The default is +# +# registry.consul.kvpath = /fabio/noroutes.html # registry.consul.service.status configures the valid service status # values for services included in the routing table. diff --git a/main.go b/main.go index dd47dc7ea..f1ccda211 100644 --- a/main.go +++ b/main.go @@ -115,6 +115,8 @@ func main() { initBackend(cfg) startAdmin(cfg) + go watchNoRouteHTML(cfg) + first := make(chan bool) go watchBackend(cfg, first) log.Print("[INFO] Waiting for first routing table") @@ -406,6 +408,21 @@ func watchBackend(cfg *config.Config, first chan bool) { } } +func watchNoRouteHTML(cfg *config.Config) { + var last string + html := registry.Default.WatchNoRouteHTML() + + for { + next := <-html + + if next == last { + continue + } + route.SetHTML(next) + last = next + } +} + func logRoutes(t route.Table, last, next, format string) { fmtDiff := func(diffs []dmp.Diff) string { var b bytes.Buffer diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index 42bc744ba..d0300188f 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -2,6 +2,7 @@ package proxy import ( "crypto/tls" + "io" "net" "net/http" "net/http/httputil" @@ -63,6 +64,10 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { t := p.Lookup(r) if t == nil { w.WriteHeader(p.Config.NoRouteStatus) + html := route.GetHTML() + if html != "" { + io.WriteString(w, html) + } return } diff --git a/registry/backend.go b/registry/backend.go index 4c734994d..bec176754 100644 --- a/registry/backend.go +++ b/registry/backend.go @@ -22,6 +22,10 @@ type Backend interface { // WatchManual watches the registry for changes in the manual // overrides and pushes them if there is a difference. WatchManual() chan string + + // WatchNoRouteHTML watches the registry for changes in the html returned + // when a requested route is not found + WatchNoRouteHTML() chan string } var Default Backend diff --git a/registry/consul/backend.go b/registry/consul/backend.go index cfa9d9e6e..921e2c222 100644 --- a/registry/consul/backend.go +++ b/registry/consul/backend.go @@ -92,6 +92,14 @@ func (b *be) WatchManual() chan string { return kv } +func (b *be) WatchNoRouteHTML() chan string { + log.Printf("[INFO] consul: Watching KV path %q", b.cfg.NoRouteHTMLPath) + + html := make(chan string) + go watchKV(b.c, b.cfg.NoRouteHTMLPath, html) + return html +} + // datacenter returns the datacenter of the local agent func datacenter(c *api.Client) (string, error) { self, err := c.Agent().Self() diff --git a/registry/static/backend.go b/registry/static/backend.go index 8c42b8819..a2fc86080 100644 --- a/registry/static/backend.go +++ b/registry/static/backend.go @@ -2,7 +2,13 @@ // backend which uses statically configured routes. package static -import "github.com/fabiolb/fabio/registry" +import ( + "io/ioutil" + "log" + + "github.com/fabiolb/fabio/registry" + "github.com/fabiolb/fabio/route" +) type be struct{} @@ -38,3 +44,14 @@ func (b *be) WatchServices() chan string { func (b *be) WatchManual() chan string { return make(chan string) } + +// WatchNoRouteHTML implementation that reads the noroute html from a +// noroute.html file if it exists +func (b *be) WatchNoRouteHTML() chan string { + data, err := ioutil.ReadFile("noroute.html") + if err != nil { + log.Println("[WARN] No noroute.html to read noroute html from") + } + route.SetHTML(string(data)) + return make(chan string) +} diff --git a/route/no_route.go b/route/no_route.go new file mode 100644 index 000000000..98566ed28 --- /dev/null +++ b/route/no_route.go @@ -0,0 +1,44 @@ +package route + +import ( + "log" + "sync" + "sync/atomic" +) + +// HTML Wrapper struct so we can store the html string in an atomic.Value +type HTML struct { + value string +} + +// html stores the no route html string +var store atomic.Value + +func init() { + store.Store(HTML{""}) +} + +// GetHTML returns the HTML for not found routes. The function is safe to be +// called from multiple goroutines. +func GetHTML() string { + return store.Load().(HTML).value +} + +// hmu guards the atomic writes in SetHTML. +var hmu sync.Mutex + +// SetHTML sets the current noroute html. The function is safe to be called from +// multiple goroutines. +func SetHTML(h string) { + hmu.Lock() + defer hmu.Unlock() + + html := HTML{h} + store.Store(html) + + if h == "" { + log.Print("[INFO] Unset noroute HTML") + } else { + log.Printf("[INFO] Set noroute HTML (%d bytes)", len(h)) + } +}