From 17363fcbcde9cf828f676e693e017e09c706a81a Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Fri, 29 Dec 2017 20:50:08 +0100 Subject: [PATCH 1/3] Issue #396: treat registry.consul.kvpath as prefix This patch extends the behavior of the registry.consul.kvpath and treats it as a prefix instead of a single key. fabio will now list the key and all available subkeys in alphabetical order and combine them for the routing table. Each subsection will get header which contains key name. Todo: the ui needs to handle that as well --- docs/content/ref/registry.consul.kvpath.md | 20 ++++++++++++++++++++ fabio.properties | 4 +++- registry/consul/kv.go | 19 ++++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/docs/content/ref/registry.consul.kvpath.md b/docs/content/ref/registry.consul.kvpath.md index 6a81c78db..488524804 100644 --- a/docs/content/ref/registry.consul.kvpath.md +++ b/docs/content/ref/registry.consul.kvpath.md @@ -8,6 +8,26 @@ The Consul KV path is watched for changes which get appended to the routing table. This allows for manual overrides and weighted round-robin routes. +As of version 1.5.7 fabio will treat the kv path as a prefix and +combine the values of the key itself and all its subkeys in +alphabetical order. + +To see all updates you may want to set [`-log.routes.format`](/ref/log.routes.format/) +to `all`. + +You can modify the content of the routes with the `consul` tool or via +the [Consul API](https://www.consul.io/api/index.html): + +``` +consul put fabio/config "route add svc /maint http://5.6.7.8:5000\nroute add svc / http://1.2.3.4:5000\n" + +# fabio >= 1.5.7 supports prefix match +consul put fabio/config/maint "route add svc /maint http://5.6.7.8:5000" +consul put fabio/config/catchall "route add svc / http://1.2.3.4:5000" + +consul delete fabio/config/maint +``` + The default is registry.consul.kvpath = /fabio/config diff --git a/fabio.properties b/fabio.properties index def4ccd78..5b302a3e5 100644 --- a/fabio.properties +++ b/fabio.properties @@ -609,7 +609,9 @@ # # The consul KV path is watched for changes which get appended to # the routing table. This allows for manual overrides and weighted -# round-robin routes. +# round-robin routes. The key itself (e.g. fabio/config) and all +# subkeys (e.g. fabio/config/foo and fabio/config/bar) are combined +# in alphabetical order. # # The default is # diff --git a/registry/consul/kv.go b/registry/consul/kv.go index bc6eabaab..1a64f4d79 100644 --- a/registry/consul/kv.go +++ b/registry/consul/kv.go @@ -15,7 +15,7 @@ func watchKV(client *api.Client, path string, config chan string) { var lastValue string for { - value, index, err := getKV(client, path, lastIndex) + value, index, err := listKV(client, path, lastIndex) if err != nil { log.Printf("[WARN] consul: Error fetching config from %s. %v", path, err) time.Sleep(time.Second) @@ -30,6 +30,23 @@ func watchKV(client *api.Client, path string, config chan string) { } } +func listKV(client *api.Client, path string, waitIndex uint64) (string, uint64, error) { + q := &api.QueryOptions{RequireConsistent: true, WaitIndex: waitIndex} + kvpairs, meta, err := client.KV().List(path, q) + if err != nil { + return "", 0, err + } + if len(kvpairs) == 0 { + return "", meta.LastIndex, nil + } + var s []string + for _, kvpair := range kvpairs { + val := "# --- " + kvpair.Key + "\n" + strings.TrimSpace(string(kvpair.Value)) + s = append(s, val) + } + return strings.Join(s, "\n\n"), meta.LastIndex, nil +} + func getKV(client *api.Client, key string, waitIndex uint64) (string, uint64, error) { q := &api.QueryOptions{RequireConsistent: true, WaitIndex: waitIndex} kvpair, meta, err := client.KV().Get(key, q) From 6738ce8426d04e5b4c772ecc76e30ce7b45739b8 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Thu, 11 Jan 2018 13:02:16 +0100 Subject: [PATCH 2/3] add ui support --- admin/api/manual.go | 10 +++++-- admin/api/paths.go | 39 +++++++++++++++++++++++++ admin/server.go | 26 +++++++++++++++-- admin/ui/logo.go | 60 +++++++++++++++++++++++++++++++++++++- admin/ui/manual.go | 59 ++++++++++++++++++++++++++++--------- admin/ui/route.go | 22 +++++++++++--- registry/backend.go | 8 +++-- registry/consul/backend.go | 15 ++++++---- registry/consul/kv.go | 16 ++++++++++ registry/static/backend.go | 8 +++-- 10 files changed, 231 insertions(+), 32 deletions(-) create mode 100644 admin/api/paths.go diff --git a/admin/api/manual.go b/admin/api/manual.go index 1e22e32ad..6acd33c2d 100644 --- a/admin/api/manual.go +++ b/admin/api/manual.go @@ -9,7 +9,9 @@ import ( ) // ManualHandler provides a fetch and update handler for the manual overrides api. -type ManualHandler struct{} +type ManualHandler struct { + BasePath string +} type manual struct { Value string `json:"value"` @@ -23,9 +25,11 @@ func (h *ManualHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + path := r.RequestURI[len(h.BasePath):] + switch r.Method { case "GET": - value, version, err := registry.Default.ReadManual() + value, version, err := registry.Default.ReadManual(path) if err != nil { log.Print("[ERROR] ", err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -43,7 +47,7 @@ func (h *ManualHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } defer r.Body.Close() - ok, err := registry.Default.WriteManual(m.Value, m.Version) + ok, err := registry.Default.WriteManual(path, m.Value, m.Version) if err != nil { log.Print("[ERROR] ", err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/admin/api/paths.go b/admin/api/paths.go new file mode 100644 index 000000000..52b9d2662 --- /dev/null +++ b/admin/api/paths.go @@ -0,0 +1,39 @@ +package api + +import ( + "log" + "net/http" + "strings" + + "github.com/fabiolb/fabio/registry" +) + +type ManualPathsHandler struct { + Prefix string +} + +func (h *ManualPathsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // we need this for testing. + // under normal circumstances this is never nil + if registry.Default == nil { + return + } + + switch r.Method { + case "GET": + paths, err := registry.Default.ManualPaths() + if err != nil { + log.Print("[ERROR] ", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + for i, p := range paths { + paths[i] = strings.TrimPrefix(p, h.Prefix) + } + writeJSON(w, r, paths) + return + + default: + http.Error(w, "not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/admin/server.go b/admin/server.go index 54957be42..03a4bca01 100644 --- a/admin/server.go +++ b/admin/server.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "fmt" "net/http" + "strings" "github.com/fabiolb/fabio/admin/api" "github.com/fabiolb/fabio/admin/ui" @@ -31,11 +32,32 @@ func (s *Server) handler() http.Handler { switch s.Access { case "ro": + mux.HandleFunc("/api/paths", forbidden) mux.HandleFunc("/api/manual", forbidden) + mux.HandleFunc("/api/manual/", forbidden) mux.HandleFunc("/manual", forbidden) + mux.HandleFunc("/manual/", forbidden) case "rw": - mux.Handle("/api/manual", &api.ManualHandler{}) - mux.Handle("/manual", &ui.ManualHandler{Color: s.Color, Title: s.Title, Version: s.Version, Commands: s.Commands}) + // for historical reasons the configured config path starts with a '/' + // but Consul treats all KV paths without a leading slash. + pathsPrefix := strings.TrimPrefix(s.Cfg.Registry.Consul.KVPath, "/") + mux.Handle("/api/paths", &api.ManualPathsHandler{Prefix: pathsPrefix}) + mux.Handle("/api/manual", &api.ManualHandler{BasePath: "/api/manual"}) + mux.Handle("/api/manual/", &api.ManualHandler{BasePath: "/api/manual"}) + mux.Handle("/manual", &ui.ManualHandler{ + BasePath: "/manual", + Color: s.Color, + Title: s.Title, + Version: s.Version, + Commands: s.Commands, + }) + mux.Handle("/manual/", &ui.ManualHandler{ + BasePath: "/manual", + Color: s.Color, + Title: s.Title, + Version: s.Version, + Commands: s.Commands, + }) } mux.Handle("/api/config", &api.ConfigHandler{Config: s.Cfg}) diff --git a/admin/ui/logo.go b/admin/ui/logo.go index 44d81ddd4..ec46272ad 100644 --- a/admin/ui/logo.go +++ b/admin/ui/logo.go @@ -4,7 +4,11 @@ import "net/http" func HandleLogo(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/svg+xml") - w.Write(logo) + if r.FormValue("format") == "bw" { + w.Write(logoBW) + } else { + w.Write(logo) + } } var logo = []byte(` @@ -29,3 +33,57 @@ var logo = []byte(` `) + +var logoBW = []byte(` + + + +Fabio + + + + + + + + + + + + + + + + + + +`) diff --git a/admin/ui/manual.go b/admin/ui/manual.go index 8c80862f4..ec18fc0a2 100644 --- a/admin/ui/manual.go +++ b/admin/ui/manual.go @@ -6,25 +6,48 @@ import ( ) type ManualHandler struct { + BasePath string Color string Title string Version string Commands string } +type paths struct { + Path string + Name string +} + func (h *ManualHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - tmplManual.ExecuteTemplate(w, "manual", h) + path := r.RequestURI[len(h.BasePath):] + data := struct { + *ManualHandler + Path string + APIPath string + }{ + ManualHandler: h, + Path: path, + APIPath: "/api/manual" + path, + } + tmplManual.ExecuteTemplate(w, "manual", data) +} + +var funcs = template.FuncMap{ + "noescape": func(str string) template.HTML { + return template.HTML(str) + }, } -var tmplManual = template.Must(template.New("manual").Parse(` +var tmplManual = template.Must(template.New("manual").Funcs(funcs).Parse(` fabio{{if .Title}} - {{.Title}}{{end}} - - - + + + +