├── dashboard ├── dist │ ├── coreos-web │ │ ├── img │ │ │ ├── favicon.png │ │ │ ├── hexagons.png │ │ │ ├── xyz-grid.png │ │ │ ├── google-plus.png │ │ │ ├── apple-touch-icon-114-precomposed.png │ │ │ ├── apple-touch-icon-144-precomposed.png │ │ │ ├── apple-touch-icon-57-precomposed.png │ │ │ ├── apple-touch-icon-72-precomposed.png │ │ │ ├── icon-add.svg │ │ │ ├── icon-right-arrow.svg │ │ │ ├── icon-back.svg │ │ │ ├── icon-delete.svg │ │ │ ├── icon-reboot.svg │ │ │ ├── globe-only.svg │ │ │ └── logo.svg │ │ └── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ ├── sourcesanspro-bold-webfont.eot │ │ │ ├── sourcesanspro-bold-webfont.ttf │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── sourcesanspro-bold-webfont.woff │ │ │ ├── sourcesanspro-light-webfont.eot │ │ │ ├── sourcesanspro-light-webfont.ttf │ │ │ ├── sourcesanspro-light-webfont.woff │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ ├── sourcesanspro-regular-webfont.eot │ │ │ ├── sourcesanspro-regular-webfont.ttf │ │ │ ├── sourcesanspro-regular-webfont.woff │ │ │ ├── sourcesanspro-extralight-webfont.eot │ │ │ ├── sourcesanspro-extralight-webfont.ttf │ │ │ ├── sourcesanspro-extralight-webfont.woff │ │ │ └── glyphicons-halflings-regular.svg │ ├── fix.js │ ├── 404.html │ ├── index.html │ └── app.js └── dashboard.go ├── third_party └── github.com │ └── gorilla │ ├── mux │ ├── README.md │ ├── bench_test.go │ ├── LICENSE │ ├── doc.go │ ├── regexp.go │ ├── mux.go │ ├── route.go │ ├── old_test.go │ └── mux_test.go │ └── context │ ├── README.md │ ├── context_test.go │ ├── LICENSE │ ├── doc.go │ └── context.go ├── .gitignore ├── config └── config.go ├── README.md ├── main.go └── LICENSE /dashboard/dist/coreos-web/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/img/favicon.png -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/hexagons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/img/hexagons.png -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/xyz-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/img/xyz-grid.png -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/google-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/img/google-plus.png -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/sourcesanspro-bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/sourcesanspro-bold-webfont.eot -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/sourcesanspro-bold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/sourcesanspro-bold-webfont.ttf -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/sourcesanspro-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/sourcesanspro-bold-webfont.woff -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/sourcesanspro-light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/sourcesanspro-light-webfont.eot -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/sourcesanspro-light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/sourcesanspro-light-webfont.ttf -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/sourcesanspro-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/sourcesanspro-light-webfont.woff -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/sourcesanspro-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/sourcesanspro-regular-webfont.eot -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/sourcesanspro-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/sourcesanspro-regular-webfont.ttf -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/sourcesanspro-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/sourcesanspro-regular-webfont.woff -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/apple-touch-icon-114-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/img/apple-touch-icon-114-precomposed.png -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/apple-touch-icon-144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/img/apple-touch-icon-144-precomposed.png -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/apple-touch-icon-57-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/img/apple-touch-icon-57-precomposed.png -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/apple-touch-icon-72-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/img/apple-touch-icon-72-precomposed.png -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/sourcesanspro-extralight-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/sourcesanspro-extralight-webfont.eot -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/sourcesanspro-extralight-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/sourcesanspro-extralight-webfont.ttf -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/sourcesanspro-extralight-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adohe-zz/etcd-dashboard/HEAD/dashboard/dist/coreos-web/fonts/sourcesanspro-extralight-webfont.woff -------------------------------------------------------------------------------- /third_party/github.com/gorilla/mux/README.md: -------------------------------------------------------------------------------- 1 | mux 2 | === 3 | [![Build Status](https://travis-ci.org/gorilla/mux.png?branch=master)](https://travis-ci.org/gorilla/mux) 4 | 5 | gorilla/mux is a powerful URL router and dispatcher. 6 | 7 | Read the full documentation here: http://www.gorillatoolkit.org/pkg/mux 8 | -------------------------------------------------------------------------------- /third_party/github.com/gorilla/context/README.md: -------------------------------------------------------------------------------- 1 | context 2 | ======= 3 | [![Build Status](https://travis-ci.org/gorilla/context.png?branch=master)](https://travis-ci.org/gorilla/context) 4 | 5 | gorilla/context is a general purpose registry for global request variables. 6 | 7 | Read the full documentation here: http://www.gorillatoolkit.org/pkg/context 8 | -------------------------------------------------------------------------------- /dashboard/dist/fix.js: -------------------------------------------------------------------------------- 1 | function showDropdownMenu(ele) { 2 | var para = ele.parentNode, 3 | clsName = para.className; 4 | 5 | if(clsName === 'co-m-cog ng-isolate-scope') { 6 | ele.parentNode.className = 'co-m-cog ng-isolate-scope open'; 7 | } else { 8 | ele.parentNode.className = 'co-m-cog ng-isolate-scope'; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | bin/ 26 | gopath/ 27 | -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/icon-add.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/icon-right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /third_party/github.com/gorilla/mux/bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "net/http" 9 | "testing" 10 | ) 11 | 12 | func BenchmarkMux(b *testing.B) { 13 | router := new(Router) 14 | handler := func(w http.ResponseWriter, r *http.Request) {} 15 | router.HandleFunc("/v1/{v1}", handler) 16 | 17 | request, _ := http.NewRequest("GET", "/v1/anything", nil) 18 | for i := 0; i < b.N; i++ { 19 | router.ServeHTTP(nil, request) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/icon-back.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "flag" 6 | ) 7 | 8 | type Config struct { 9 | 10 | Host string `toml:"host"` 11 | Port string `toml:"port"` 12 | } 13 | 14 | func New() *Config { 15 | c := new(Config) 16 | c.Host = "127.0.0.1" 17 | c.Port = "8080" 18 | 19 | return c 20 | } 21 | 22 | // Loads configuration from command line flags. 23 | func (c *Config) LoadFlags(arguments []string) error { 24 | 25 | f := flag.NewFlagSet(os.Args[0], flag.ContinueOnError) 26 | f.StringVar(&c.Host, "host", c.Host, "") 27 | f.StringVar(&c.Port, "port", c.Port, "") 28 | 29 | if err := f.Parse(arguments); err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dashboard 2 | Dashboard for etcd. You can still use the cool etcd dashboard with etcd 2.*. 3 | 4 | ## Getting dashboard 5 | 6 | The latest release and setup instructions are available at [Github](https://github.com/AdoHe/etcd-dashboard). 7 | 8 | ## Building 9 | 10 | You can build dashboard from source: 11 | 12 | ``` 13 | git clone git@github.com:AdoHe/etcd-dashboard.git 14 | cd etcd-dashboard 15 | ./build 16 | ``` 17 | This will generate binary called `./bin/etcd-dashboard`. 18 | 19 | ## Running 20 | 21 | Just run this command: 22 | 23 | ``` 24 | ./bin/etcd-dashboard -host 127.0.0.1 -port 8080 25 | ``` 26 | And then type this `http://127.0.0.1:8080/mod/dashboard` address in your favorite browser, you will see the awesome etcd dashboard.**Just try it** 27 | -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/icon-delete.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "fmt" 8 | 9 | "github.com/AdoHe/etcd-dashboard/third_party/github.com/gorilla/mux" 10 | "github.com/AdoHe/etcd-dashboard/dashboard" 11 | "github.com/AdoHe/etcd-dashboard/config" 12 | ) 13 | 14 | func main() { 15 | 16 | var config = config.New() 17 | 18 | if err := config.LoadFlags(os.Args[1:]); err != nil { 19 | fmt.Println(err.Error() + "\n") 20 | os.Exit(1) 21 | } 22 | 23 | r := mux.NewRouter() 24 | r.PathPrefix("/mod").Handler(http.StripPrefix("/mod", dashboard.HTTPHandler())) 25 | 26 | http.Handle("/", r) 27 | var addr string = config.Host + ":" + config.Port 28 | err := http.ListenAndServe(addr, nil) 29 | if err != nil { 30 | log.Fatal("ListenAndServe: ", err) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /dashboard/dashboard.go: -------------------------------------------------------------------------------- 1 | package dashboard 2 | 3 | import ( 4 | "net/http" 5 | "path" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/AdoHe/etcd-dashboard/third_party/github.com/gorilla/mux" 10 | ) 11 | 12 | var pwd string 13 | 14 | func addSlash(w http.ResponseWriter, req *http.Request) { 15 | http.Redirect(w, req, path.Join("mod", req.URL.Path)+"/", 302) 16 | return 17 | } 18 | 19 | func indexPage(w http.ResponseWriter, req *http.Request) { 20 | fmt.Printf("%s", "serve index page") 21 | http.ServeFile(w, req, path.Join(pwd, "/dashboard/dist", "index.html")) 22 | } 23 | 24 | func HttpHandler() (handler http.Handler) { 25 | handler = http.FileServer(http.Dir(path.Join(pwd, "/dashboard/dist"))) 26 | return handler 27 | } 28 | 29 | func HTTPHandler() http.Handler { 30 | pwd, _ = os.Getwd() 31 | r := mux.NewRouter() 32 | r.HandleFunc("/dashboard", addSlash) 33 | 34 | r.PathPrefix("/dashboard/static/").Handler(http.StripPrefix("/dashboard/static/", HttpHandler())) 35 | r.HandleFunc("/dashboard{path:.*}", indexPage) 36 | 37 | return r 38 | } 39 | -------------------------------------------------------------------------------- /dashboard/dist/404.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
6 |

Page Not Found

7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 |
404
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 |
30 | 31 |
32 | -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/icon-reboot.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /dashboard/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | etcd 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 |
24 |
25 | 26 | 27 | Documentation 28 | IRC #coreos 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /third_party/github.com/gorilla/context/context_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package context 6 | 7 | import ( 8 | "net/http" 9 | "testing" 10 | ) 11 | 12 | type keyType int 13 | 14 | const ( 15 | key1 keyType = iota 16 | key2 17 | ) 18 | 19 | func TestContext(t *testing.T) { 20 | assertEqual := func(val interface{}, exp interface{}) { 21 | if val != exp { 22 | t.Errorf("Expected %v, got %v.", exp, val) 23 | } 24 | } 25 | 26 | r, _ := http.NewRequest("GET", "http://localhost:8080/", nil) 27 | 28 | // Get() 29 | assertEqual(Get(r, key1), nil) 30 | 31 | // Set() 32 | Set(r, key1, "1") 33 | assertEqual(Get(r, key1), "1") 34 | assertEqual(len(data[r]), 1) 35 | 36 | Set(r, key2, "2") 37 | assertEqual(Get(r, key2), "2") 38 | assertEqual(len(data[r]), 2) 39 | 40 | //GetOk 41 | value, ok := GetOk(r, key1) 42 | assertEqual(value, "1") 43 | assertEqual(ok, true) 44 | 45 | value, ok = GetOk(r, "not exists") 46 | assertEqual(value, nil) 47 | assertEqual(ok, false) 48 | 49 | Set(r, "nil value", nil) 50 | value, ok = GetOk(r, "nil value") 51 | assertEqual(value, nil) 52 | assertEqual(ok, true) 53 | 54 | // Delete() 55 | Delete(r, key1) 56 | assertEqual(Get(r, key1), nil) 57 | assertEqual(len(data[r]), 2) 58 | 59 | Delete(r, key2) 60 | assertEqual(Get(r, key2), nil) 61 | assertEqual(len(data[r]), 1) 62 | 63 | // Clear() 64 | Clear(r) 65 | assertEqual(len(data), 0) 66 | } 67 | -------------------------------------------------------------------------------- /third_party/github.com/gorilla/context/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Rodrigo Moraes. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /third_party/github.com/gorilla/mux/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Rodrigo Moraes. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/globe-only.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 10 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /third_party/github.com/gorilla/context/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package gorilla/context stores values shared during a request lifetime. 7 | 8 | For example, a router can set variables extracted from the URL and later 9 | application handlers can access those values, or it can be used to store 10 | sessions values to be saved at the end of a request. There are several 11 | others common uses. 12 | 13 | The idea was posted by Brad Fitzpatrick to the go-nuts mailing list: 14 | 15 | http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53 16 | 17 | Here's the basic usage: first define the keys that you will need. The key 18 | type is interface{} so a key can be of any type that supports equality. 19 | Here we define a key using a custom int type to avoid name collisions: 20 | 21 | package foo 22 | 23 | import ( 24 | "github.com/gorilla/context" 25 | ) 26 | 27 | type key int 28 | 29 | const MyKey key = 0 30 | 31 | Then set a variable. Variables are bound to an http.Request object, so you 32 | need a request instance to set a value: 33 | 34 | context.Set(r, MyKey, "bar") 35 | 36 | The application can later access the variable using the same key you provided: 37 | 38 | func MyHandler(w http.ResponseWriter, r *http.Request) { 39 | // val is "bar". 40 | val := context.Get(r, foo.MyKey) 41 | 42 | // returns ("bar", true) 43 | val, ok := context.GetOk(r, foo.MyKey) 44 | // ... 45 | } 46 | 47 | And that's all about the basic usage. We discuss some other ideas below. 48 | 49 | Any type can be stored in the context. To enforce a given type, make the key 50 | private and wrap Get() and Set() to accept and return values of a specific 51 | type: 52 | 53 | type key int 54 | 55 | const mykey key = 0 56 | 57 | // GetMyKey returns a value for this package from the request values. 58 | func GetMyKey(r *http.Request) SomeType { 59 | if rv := context.Get(r, mykey); rv != nil { 60 | return rv.(SomeType) 61 | } 62 | return nil 63 | } 64 | 65 | // SetMyKey sets a value for this package in the request values. 66 | func SetMyKey(r *http.Request, val SomeType) { 67 | context.Set(r, mykey, val) 68 | } 69 | 70 | Variables must be cleared at the end of a request, to remove all values 71 | that were stored. This can be done in an http.Handler, after a request was 72 | served. Just call Clear() passing the request: 73 | 74 | context.Clear(r) 75 | 76 | ...or use ClearHandler(), which conveniently wraps an http.Handler to clear 77 | variables at the end of a request lifetime. 78 | 79 | The Routers from the packages gorilla/mux and gorilla/pat call Clear() 80 | so if you are using either of them you don't need to clear the context manually. 81 | */ 82 | package context 83 | -------------------------------------------------------------------------------- /third_party/github.com/gorilla/context/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package context 6 | 7 | import ( 8 | "net/http" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | var ( 14 | mutex sync.Mutex 15 | data = make(map[*http.Request]map[interface{}]interface{}) 16 | datat = make(map[*http.Request]int64) 17 | ) 18 | 19 | // Set stores a value for a given key in a given request. 20 | func Set(r *http.Request, key, val interface{}) { 21 | mutex.Lock() 22 | defer mutex.Unlock() 23 | if data[r] == nil { 24 | data[r] = make(map[interface{}]interface{}) 25 | datat[r] = time.Now().Unix() 26 | } 27 | data[r][key] = val 28 | } 29 | 30 | // Get returns a value stored for a given key in a given request. 31 | func Get(r *http.Request, key interface{}) interface{} { 32 | mutex.Lock() 33 | defer mutex.Unlock() 34 | if data[r] != nil { 35 | return data[r][key] 36 | } 37 | return nil 38 | } 39 | 40 | // GetOk returns stored value and presence state like multi-value return of map access. 41 | func GetOk(r *http.Request, key interface{}) (interface{}, bool) { 42 | mutex.Lock() 43 | defer mutex.Unlock() 44 | if _, ok := data[r]; ok { 45 | value, ok := data[r][key] 46 | return value, ok 47 | } 48 | return nil, false 49 | } 50 | 51 | // Delete removes a value stored for a given key in a given request. 52 | func Delete(r *http.Request, key interface{}) { 53 | mutex.Lock() 54 | defer mutex.Unlock() 55 | if data[r] != nil { 56 | delete(data[r], key) 57 | } 58 | } 59 | 60 | // Clear removes all values stored for a given request. 61 | // 62 | // This is usually called by a handler wrapper to clean up request 63 | // variables at the end of a request lifetime. See ClearHandler(). 64 | func Clear(r *http.Request) { 65 | mutex.Lock() 66 | defer mutex.Unlock() 67 | clear(r) 68 | } 69 | 70 | // clear is Clear without the lock. 71 | func clear(r *http.Request) { 72 | delete(data, r) 73 | delete(datat, r) 74 | } 75 | 76 | // Purge removes request data stored for longer than maxAge, in seconds. 77 | // It returns the amount of requests removed. 78 | // 79 | // If maxAge <= 0, all request data is removed. 80 | // 81 | // This is only used for sanity check: in case context cleaning was not 82 | // properly set some request data can be kept forever, consuming an increasing 83 | // amount of memory. In case this is detected, Purge() must be called 84 | // periodically until the problem is fixed. 85 | func Purge(maxAge int) int { 86 | mutex.Lock() 87 | defer mutex.Unlock() 88 | count := 0 89 | if maxAge <= 0 { 90 | count = len(data) 91 | data = make(map[*http.Request]map[interface{}]interface{}) 92 | datat = make(map[*http.Request]int64) 93 | } else { 94 | min := time.Now().Unix() - int64(maxAge) 95 | for r := range data { 96 | if datat[r] < min { 97 | clear(r) 98 | count++ 99 | } 100 | } 101 | } 102 | return count 103 | } 104 | 105 | // ClearHandler wraps an http.Handler and clears request values at the end 106 | // of a request lifetime. 107 | func ClearHandler(h http.Handler) http.Handler { 108 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 109 | defer Clear(r) 110 | h.ServeHTTP(w, r) 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/img/logo.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 7 | 12 | 17 | 18 | 19 | 23 | 27 | 30 | 34 | 38 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /third_party/github.com/gorilla/mux/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package gorilla/mux implements a request router and dispatcher. 7 | 8 | The name mux stands for "HTTP request multiplexer". Like the standard 9 | http.ServeMux, mux.Router matches incoming requests against a list of 10 | registered routes and calls a handler for the route that matches the URL 11 | or other conditions. The main features are: 12 | 13 | * Requests can be matched based on URL host, path, path prefix, schemes, 14 | header and query values, HTTP methods or using custom matchers. 15 | * URL hosts and paths can have variables with an optional regular 16 | expression. 17 | * Registered URLs can be built, or "reversed", which helps maintaining 18 | references to resources. 19 | * Routes can be used as subrouters: nested routes are only tested if the 20 | parent route matches. This is useful to define groups of routes that 21 | share common conditions like a host, a path prefix or other repeated 22 | attributes. As a bonus, this optimizes request matching. 23 | * It implements the http.Handler interface so it is compatible with the 24 | standard http.ServeMux. 25 | 26 | Let's start registering a couple of URL paths and handlers: 27 | 28 | func main() { 29 | r := mux.NewRouter() 30 | r.HandleFunc("/", HomeHandler) 31 | r.HandleFunc("/products", ProductsHandler) 32 | r.HandleFunc("/articles", ArticlesHandler) 33 | http.Handle("/", r) 34 | } 35 | 36 | Here we register three routes mapping URL paths to handlers. This is 37 | equivalent to how http.HandleFunc() works: if an incoming request URL matches 38 | one of the paths, the corresponding handler is called passing 39 | (http.ResponseWriter, *http.Request) as parameters. 40 | 41 | Paths can have variables. They are defined using the format {name} or 42 | {name:pattern}. If a regular expression pattern is not defined, the matched 43 | variable will be anything until the next slash. For example: 44 | 45 | r := mux.NewRouter() 46 | r.HandleFunc("/products/{key}", ProductHandler) 47 | r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) 48 | r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) 49 | 50 | The names are used to create a map of route variables which can be retrieved 51 | calling mux.Vars(): 52 | 53 | vars := mux.Vars(request) 54 | category := vars["category"] 55 | 56 | And this is all you need to know about the basic usage. More advanced options 57 | are explained below. 58 | 59 | Routes can also be restricted to a domain or subdomain. Just define a host 60 | pattern to be matched. They can also have variables: 61 | 62 | r := mux.NewRouter() 63 | // Only matches if domain is "www.domain.com". 64 | r.Host("www.domain.com") 65 | // Matches a dynamic subdomain. 66 | r.Host("{subdomain:[a-z]+}.domain.com") 67 | 68 | There are several other matchers that can be added. To match path prefixes: 69 | 70 | r.PathPrefix("/products/") 71 | 72 | ...or HTTP methods: 73 | 74 | r.Methods("GET", "POST") 75 | 76 | ...or URL schemes: 77 | 78 | r.Schemes("https") 79 | 80 | ...or header values: 81 | 82 | r.Headers("X-Requested-With", "XMLHttpRequest") 83 | 84 | ...or query values: 85 | 86 | r.Queries("key", "value") 87 | 88 | ...or to use a custom matcher function: 89 | 90 | r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { 91 | return r.ProtoMajor == 0 92 | }) 93 | 94 | ...and finally, it is possible to combine several matchers in a single route: 95 | 96 | r.HandleFunc("/products", ProductsHandler). 97 | Host("www.domain.com"). 98 | Methods("GET"). 99 | Schemes("http") 100 | 101 | Setting the same matching conditions again and again can be boring, so we have 102 | a way to group several routes that share the same requirements. 103 | We call it "subrouting". 104 | 105 | For example, let's say we have several URLs that should only match when the 106 | host is "www.domain.com". Create a route for that host and get a "subrouter" 107 | from it: 108 | 109 | r := mux.NewRouter() 110 | s := r.Host("www.domain.com").Subrouter() 111 | 112 | Then register routes in the subrouter: 113 | 114 | s.HandleFunc("/products/", ProductsHandler) 115 | s.HandleFunc("/products/{key}", ProductHandler) 116 | s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) 117 | 118 | The three URL paths we registered above will only be tested if the domain is 119 | "www.domain.com", because the subrouter is tested first. This is not 120 | only convenient, but also optimizes request matching. You can create 121 | subrouters combining any attribute matchers accepted by a route. 122 | 123 | Subrouters can be used to create domain or path "namespaces": you define 124 | subrouters in a central place and then parts of the app can register its 125 | paths relatively to a given subrouter. 126 | 127 | There's one more thing about subroutes. When a subrouter has a path prefix, 128 | the inner routes use it as base for their paths: 129 | 130 | r := mux.NewRouter() 131 | s := r.PathPrefix("/products").Subrouter() 132 | // "/products/" 133 | s.HandleFunc("/", ProductsHandler) 134 | // "/products/{key}/" 135 | s.HandleFunc("/{key}/", ProductHandler) 136 | // "/products/{key}/details" 137 | s.HandleFunc("/{key}/details", ProductDetailsHandler) 138 | 139 | Now let's see how to build registered URLs. 140 | 141 | Routes can be named. All routes that define a name can have their URLs built, 142 | or "reversed". We define a name calling Name() on a route. For example: 143 | 144 | r := mux.NewRouter() 145 | r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 146 | Name("article") 147 | 148 | To build a URL, get the route and call the URL() method, passing a sequence of 149 | key/value pairs for the route variables. For the previous route, we would do: 150 | 151 | url, err := r.Get("article").URL("category", "technology", "id", "42") 152 | 153 | ...and the result will be a url.URL with the following path: 154 | 155 | "/articles/technology/42" 156 | 157 | This also works for host variables: 158 | 159 | r := mux.NewRouter() 160 | r.Host("{subdomain}.domain.com"). 161 | Path("/articles/{category}/{id:[0-9]+}"). 162 | HandlerFunc(ArticleHandler). 163 | Name("article") 164 | 165 | // url.String() will be "http://news.domain.com/articles/technology/42" 166 | url, err := r.Get("article").URL("subdomain", "news", 167 | "category", "technology", 168 | "id", "42") 169 | 170 | All variables defined in the route are required, and their values must 171 | conform to the corresponding patterns. These requirements guarantee that a 172 | generated URL will always match a registered route -- the only exception is 173 | for explicitly defined "build-only" routes which never match. 174 | 175 | There's also a way to build only the URL host or path for a route: 176 | use the methods URLHost() or URLPath() instead. For the previous route, 177 | we would do: 178 | 179 | // "http://news.domain.com/" 180 | host, err := r.Get("article").URLHost("subdomain", "news") 181 | 182 | // "/articles/technology/42" 183 | path, err := r.Get("article").URLPath("category", "technology", "id", "42") 184 | 185 | And if you use subrouters, host and path defined separately can be built 186 | as well: 187 | 188 | r := mux.NewRouter() 189 | s := r.Host("{subdomain}.domain.com").Subrouter() 190 | s.Path("/articles/{category}/{id:[0-9]+}"). 191 | HandlerFunc(ArticleHandler). 192 | Name("article") 193 | 194 | // "http://news.domain.com/articles/technology/42" 195 | url, err := r.Get("article").URL("subdomain", "news", 196 | "category", "technology", 197 | "id", "42") 198 | */ 199 | package mux 200 | -------------------------------------------------------------------------------- /third_party/github.com/gorilla/mux/regexp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "regexp" 13 | "strings" 14 | ) 15 | 16 | // newRouteRegexp parses a route template and returns a routeRegexp, 17 | // used to match a host or path. 18 | // 19 | // It will extract named variables, assemble a regexp to be matched, create 20 | // a "reverse" template to build URLs and compile regexps to validate variable 21 | // values used in URL building. 22 | // 23 | // Previously we accepted only Python-like identifiers for variable 24 | // names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that 25 | // name and pattern can't be empty, and names can't contain a colon. 26 | func newRouteRegexp(tpl string, matchHost, matchPrefix, strictSlash bool) (*routeRegexp, error) { 27 | // Check if it is well-formed. 28 | idxs, errBraces := braceIndices(tpl) 29 | if errBraces != nil { 30 | return nil, errBraces 31 | } 32 | // Backup the original. 33 | template := tpl 34 | // Now let's parse it. 35 | defaultPattern := "[^/]+" 36 | if matchHost { 37 | defaultPattern = "[^.]+" 38 | matchPrefix, strictSlash = false, false 39 | } 40 | if matchPrefix { 41 | strictSlash = false 42 | } 43 | // Set a flag for strictSlash. 44 | endSlash := false 45 | if strictSlash && strings.HasSuffix(tpl, "/") { 46 | tpl = tpl[:len(tpl)-1] 47 | endSlash = true 48 | } 49 | varsN := make([]string, len(idxs)/2) 50 | varsR := make([]*regexp.Regexp, len(idxs)/2) 51 | pattern := bytes.NewBufferString("^") 52 | reverse := bytes.NewBufferString("") 53 | var end int 54 | var err error 55 | for i := 0; i < len(idxs); i += 2 { 56 | // Set all values we are interested in. 57 | raw := tpl[end:idxs[i]] 58 | end = idxs[i+1] 59 | parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) 60 | name := parts[0] 61 | patt := defaultPattern 62 | if len(parts) == 2 { 63 | patt = parts[1] 64 | } 65 | // Name or pattern can't be empty. 66 | if name == "" || patt == "" { 67 | return nil, fmt.Errorf("mux: missing name or pattern in %q", 68 | tpl[idxs[i]:end]) 69 | } 70 | // Build the regexp pattern. 71 | fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt) 72 | // Build the reverse template. 73 | fmt.Fprintf(reverse, "%s%%s", raw) 74 | // Append variable name and compiled pattern. 75 | varsN[i/2] = name 76 | varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) 77 | if err != nil { 78 | return nil, err 79 | } 80 | } 81 | // Add the remaining. 82 | raw := tpl[end:] 83 | pattern.WriteString(regexp.QuoteMeta(raw)) 84 | if strictSlash { 85 | pattern.WriteString("[/]?") 86 | } 87 | if !matchPrefix { 88 | pattern.WriteByte('$') 89 | } 90 | reverse.WriteString(raw) 91 | if endSlash { 92 | reverse.WriteByte('/') 93 | } 94 | // Compile full regexp. 95 | reg, errCompile := regexp.Compile(pattern.String()) 96 | if errCompile != nil { 97 | return nil, errCompile 98 | } 99 | // Done! 100 | return &routeRegexp{ 101 | template: template, 102 | matchHost: matchHost, 103 | regexp: reg, 104 | reverse: reverse.String(), 105 | varsN: varsN, 106 | varsR: varsR, 107 | }, nil 108 | } 109 | 110 | // routeRegexp stores a regexp to match a host or path and information to 111 | // collect and validate route variables. 112 | type routeRegexp struct { 113 | // The unmodified template. 114 | template string 115 | // True for host match, false for path match. 116 | matchHost bool 117 | // Expanded regexp. 118 | regexp *regexp.Regexp 119 | // Reverse template. 120 | reverse string 121 | // Variable names. 122 | varsN []string 123 | // Variable regexps (validators). 124 | varsR []*regexp.Regexp 125 | } 126 | 127 | // Match matches the regexp against the URL host or path. 128 | func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { 129 | if !r.matchHost { 130 | return r.regexp.MatchString(req.URL.Path) 131 | } 132 | return r.regexp.MatchString(getHost(req)) 133 | } 134 | 135 | // url builds a URL part using the given values. 136 | func (r *routeRegexp) url(pairs ...string) (string, error) { 137 | values, err := mapFromPairs(pairs...) 138 | if err != nil { 139 | return "", err 140 | } 141 | urlValues := make([]interface{}, len(r.varsN)) 142 | for k, v := range r.varsN { 143 | value, ok := values[v] 144 | if !ok { 145 | return "", fmt.Errorf("mux: missing route variable %q", v) 146 | } 147 | urlValues[k] = value 148 | } 149 | rv := fmt.Sprintf(r.reverse, urlValues...) 150 | if !r.regexp.MatchString(rv) { 151 | // The URL is checked against the full regexp, instead of checking 152 | // individual variables. This is faster but to provide a good error 153 | // message, we check individual regexps if the URL doesn't match. 154 | for k, v := range r.varsN { 155 | if !r.varsR[k].MatchString(values[v]) { 156 | return "", fmt.Errorf( 157 | "mux: variable %q doesn't match, expected %q", values[v], 158 | r.varsR[k].String()) 159 | } 160 | } 161 | } 162 | return rv, nil 163 | } 164 | 165 | // braceIndices returns the first level curly brace indices from a string. 166 | // It returns an error in case of unbalanced braces. 167 | func braceIndices(s string) ([]int, error) { 168 | var level, idx int 169 | idxs := make([]int, 0) 170 | for i := 0; i < len(s); i++ { 171 | switch s[i] { 172 | case '{': 173 | if level++; level == 1 { 174 | idx = i 175 | } 176 | case '}': 177 | if level--; level == 0 { 178 | idxs = append(idxs, idx, i+1) 179 | } else if level < 0 { 180 | return nil, fmt.Errorf("mux: unbalanced braces in %q", s) 181 | } 182 | } 183 | } 184 | if level != 0 { 185 | return nil, fmt.Errorf("mux: unbalanced braces in %q", s) 186 | } 187 | return idxs, nil 188 | } 189 | 190 | // ---------------------------------------------------------------------------- 191 | // routeRegexpGroup 192 | // ---------------------------------------------------------------------------- 193 | 194 | // routeRegexpGroup groups the route matchers that carry variables. 195 | type routeRegexpGroup struct { 196 | host *routeRegexp 197 | path *routeRegexp 198 | } 199 | 200 | // setMatch extracts the variables from the URL once a route matches. 201 | func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { 202 | // Store host variables. 203 | if v.host != nil { 204 | hostVars := v.host.regexp.FindStringSubmatch(getHost(req)) 205 | if hostVars != nil { 206 | for k, v := range v.host.varsN { 207 | m.Vars[v] = hostVars[k+1] 208 | } 209 | } 210 | } 211 | // Store path variables. 212 | if v.path != nil { 213 | pathVars := v.path.regexp.FindStringSubmatch(req.URL.Path) 214 | if pathVars != nil { 215 | for k, v := range v.path.varsN { 216 | m.Vars[v] = pathVars[k+1] 217 | } 218 | // Check if we should redirect. 219 | if r.strictSlash { 220 | p1 := strings.HasSuffix(req.URL.Path, "/") 221 | p2 := strings.HasSuffix(v.path.template, "/") 222 | if p1 != p2 { 223 | u, _ := url.Parse(req.URL.String()) 224 | if p1 { 225 | u.Path = u.Path[:len(u.Path)-1] 226 | } else { 227 | u.Path += "/" 228 | } 229 | m.Handler = http.RedirectHandler(u.String(), 301) 230 | } 231 | } 232 | } 233 | } 234 | } 235 | 236 | // getHost tries its best to return the request host. 237 | func getHost(r *http.Request) string { 238 | if !r.URL.IsAbs() { 239 | host := r.Host 240 | // Slice off any port information. 241 | if i := strings.Index(host, ":"); i != -1 { 242 | host = host[:i] 243 | } 244 | return host 245 | } 246 | return r.URL.Host 247 | } 248 | -------------------------------------------------------------------------------- /third_party/github.com/gorilla/mux/mux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "path" 11 | 12 | "github.com/AdoHe/etcd-dashboard/third_party/github.com/gorilla/context" 13 | ) 14 | 15 | // NewRouter returns a new router instance. 16 | func NewRouter() *Router { 17 | return &Router{namedRoutes: make(map[string]*Route), KeepContext: false} 18 | } 19 | 20 | // Router registers routes to be matched and dispatches a handler. 21 | // 22 | // It implements the http.Handler interface, so it can be registered to serve 23 | // requests: 24 | // 25 | // var router = mux.NewRouter() 26 | // 27 | // func main() { 28 | // http.Handle("/", router) 29 | // } 30 | // 31 | // Or, for Google App Engine, register it in a init() function: 32 | // 33 | // func init() { 34 | // http.Handle("/", router) 35 | // } 36 | // 37 | // This will send all incoming requests to the router. 38 | type Router struct { 39 | // Configurable Handler to be used when no route matches. 40 | NotFoundHandler http.Handler 41 | // Parent route, if this is a subrouter. 42 | parent parentRoute 43 | // Routes to be matched, in order. 44 | routes []*Route 45 | // Routes by name for URL building. 46 | namedRoutes map[string]*Route 47 | // See Router.StrictSlash(). This defines the flag for new routes. 48 | strictSlash bool 49 | // If true, do not clear the request context after handling the request 50 | KeepContext bool 51 | } 52 | 53 | // Match matches registered routes against the request. 54 | func (r *Router) Match(req *http.Request, match *RouteMatch) bool { 55 | for _, route := range r.routes { 56 | if route.Match(req, match) { 57 | return true 58 | } 59 | } 60 | return false 61 | } 62 | 63 | // ServeHTTP dispatches the handler registered in the matched route. 64 | // 65 | // When there is a match, the route variables can be retrieved calling 66 | // mux.Vars(request). 67 | func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 68 | // Clean path to canonical form and redirect. 69 | if p := cleanPath(req.URL.Path); p != req.URL.Path { 70 | 71 | // Added 3 lines (Philip Schlump) - It was droping the query string and #whatever from query. 72 | // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: 73 | // http://code.google.com/p/go/issues/detail?id=5252 74 | url := *req.URL 75 | url.Path = p 76 | p = url.String() 77 | 78 | w.Header().Set("Location", p) 79 | w.WriteHeader(http.StatusMovedPermanently) 80 | return 81 | } 82 | var match RouteMatch 83 | var handler http.Handler 84 | if r.Match(req, &match) { 85 | handler = match.Handler 86 | setVars(req, match.Vars) 87 | setCurrentRoute(req, match.Route) 88 | } 89 | if handler == nil { 90 | if r.NotFoundHandler == nil { 91 | r.NotFoundHandler = http.NotFoundHandler() 92 | } 93 | handler = r.NotFoundHandler 94 | } 95 | if !r.KeepContext { 96 | defer context.Clear(req) 97 | } 98 | handler.ServeHTTP(w, req) 99 | } 100 | 101 | // Get returns a route registered with the given name. 102 | func (r *Router) Get(name string) *Route { 103 | return r.getNamedRoutes()[name] 104 | } 105 | 106 | // GetRoute returns a route registered with the given name. This method 107 | // was renamed to Get() and remains here for backwards compatibility. 108 | func (r *Router) GetRoute(name string) *Route { 109 | return r.getNamedRoutes()[name] 110 | } 111 | 112 | // StrictSlash defines the slash behavior for new routes. 113 | // 114 | // When true, if the route path is "/path/", accessing "/path" will redirect 115 | // to the former and vice versa. 116 | // 117 | // Special case: when a route sets a path prefix, strict slash is 118 | // automatically set to false for that route because the redirect behavior 119 | // can't be determined for prefixes. 120 | func (r *Router) StrictSlash(value bool) *Router { 121 | r.strictSlash = value 122 | return r 123 | } 124 | 125 | // ---------------------------------------------------------------------------- 126 | // parentRoute 127 | // ---------------------------------------------------------------------------- 128 | 129 | // getNamedRoutes returns the map where named routes are registered. 130 | func (r *Router) getNamedRoutes() map[string]*Route { 131 | if r.namedRoutes == nil { 132 | if r.parent != nil { 133 | r.namedRoutes = r.parent.getNamedRoutes() 134 | } else { 135 | r.namedRoutes = make(map[string]*Route) 136 | } 137 | } 138 | return r.namedRoutes 139 | } 140 | 141 | // getRegexpGroup returns regexp definitions from the parent route, if any. 142 | func (r *Router) getRegexpGroup() *routeRegexpGroup { 143 | if r.parent != nil { 144 | return r.parent.getRegexpGroup() 145 | } 146 | return nil 147 | } 148 | 149 | // ---------------------------------------------------------------------------- 150 | // Route factories 151 | // ---------------------------------------------------------------------------- 152 | 153 | // NewRoute registers an empty route. 154 | func (r *Router) NewRoute() *Route { 155 | route := &Route{parent: r, strictSlash: r.strictSlash} 156 | r.routes = append(r.routes, route) 157 | return route 158 | } 159 | 160 | // Handle registers a new route with a matcher for the URL path. 161 | // See Route.Path() and Route.Handler(). 162 | func (r *Router) Handle(path string, handler http.Handler) *Route { 163 | return r.NewRoute().Path(path).Handler(handler) 164 | } 165 | 166 | // HandleFunc registers a new route with a matcher for the URL path. 167 | // See Route.Path() and Route.HandlerFunc(). 168 | func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, 169 | *http.Request)) *Route { 170 | return r.NewRoute().Path(path).HandlerFunc(f) 171 | } 172 | 173 | // Headers registers a new route with a matcher for request header values. 174 | // See Route.Headers(). 175 | func (r *Router) Headers(pairs ...string) *Route { 176 | return r.NewRoute().Headers(pairs...) 177 | } 178 | 179 | // Host registers a new route with a matcher for the URL host. 180 | // See Route.Host(). 181 | func (r *Router) Host(tpl string) *Route { 182 | return r.NewRoute().Host(tpl) 183 | } 184 | 185 | // MatcherFunc registers a new route with a custom matcher function. 186 | // See Route.MatcherFunc(). 187 | func (r *Router) MatcherFunc(f MatcherFunc) *Route { 188 | return r.NewRoute().MatcherFunc(f) 189 | } 190 | 191 | // Methods registers a new route with a matcher for HTTP methods. 192 | // See Route.Methods(). 193 | func (r *Router) Methods(methods ...string) *Route { 194 | return r.NewRoute().Methods(methods...) 195 | } 196 | 197 | // Path registers a new route with a matcher for the URL path. 198 | // See Route.Path(). 199 | func (r *Router) Path(tpl string) *Route { 200 | return r.NewRoute().Path(tpl) 201 | } 202 | 203 | // PathPrefix registers a new route with a matcher for the URL path prefix. 204 | // See Route.PathPrefix(). 205 | func (r *Router) PathPrefix(tpl string) *Route { 206 | return r.NewRoute().PathPrefix(tpl) 207 | } 208 | 209 | // Queries registers a new route with a matcher for URL query values. 210 | // See Route.Queries(). 211 | func (r *Router) Queries(pairs ...string) *Route { 212 | return r.NewRoute().Queries(pairs...) 213 | } 214 | 215 | // Schemes registers a new route with a matcher for URL schemes. 216 | // See Route.Schemes(). 217 | func (r *Router) Schemes(schemes ...string) *Route { 218 | return r.NewRoute().Schemes(schemes...) 219 | } 220 | 221 | // ---------------------------------------------------------------------------- 222 | // Context 223 | // ---------------------------------------------------------------------------- 224 | 225 | // RouteMatch stores information about a matched route. 226 | type RouteMatch struct { 227 | Route *Route 228 | Handler http.Handler 229 | Vars map[string]string 230 | } 231 | 232 | type contextKey int 233 | 234 | const ( 235 | varsKey contextKey = iota 236 | routeKey 237 | ) 238 | 239 | // Vars returns the route variables for the current request, if any. 240 | func Vars(r *http.Request) map[string]string { 241 | if rv := context.Get(r, varsKey); rv != nil { 242 | return rv.(map[string]string) 243 | } 244 | return nil 245 | } 246 | 247 | // CurrentRoute returns the matched route for the current request, if any. 248 | func CurrentRoute(r *http.Request) *Route { 249 | if rv := context.Get(r, routeKey); rv != nil { 250 | return rv.(*Route) 251 | } 252 | return nil 253 | } 254 | 255 | func setVars(r *http.Request, val interface{}) { 256 | context.Set(r, varsKey, val) 257 | } 258 | 259 | func setCurrentRoute(r *http.Request, val interface{}) { 260 | context.Set(r, routeKey, val) 261 | } 262 | 263 | // ---------------------------------------------------------------------------- 264 | // Helpers 265 | // ---------------------------------------------------------------------------- 266 | 267 | // cleanPath returns the canonical path for p, eliminating . and .. elements. 268 | // Borrowed from the net/http package. 269 | func cleanPath(p string) string { 270 | if p == "" { 271 | return "/" 272 | } 273 | if p[0] != '/' { 274 | p = "/" + p 275 | } 276 | np := path.Clean(p) 277 | // path.Clean removes trailing slash except for root; 278 | // put the trailing slash back if necessary. 279 | if p[len(p)-1] == '/' && np != "/" { 280 | np += "/" 281 | } 282 | return np 283 | } 284 | 285 | // uniqueVars returns an error if two slices contain duplicated strings. 286 | func uniqueVars(s1, s2 []string) error { 287 | for _, v1 := range s1 { 288 | for _, v2 := range s2 { 289 | if v1 == v2 { 290 | return fmt.Errorf("mux: duplicated route variable %q", v2) 291 | } 292 | } 293 | } 294 | return nil 295 | } 296 | 297 | // mapFromPairs converts variadic string parameters to a string map. 298 | func mapFromPairs(pairs ...string) (map[string]string, error) { 299 | length := len(pairs) 300 | if length%2 != 0 { 301 | return nil, fmt.Errorf( 302 | "mux: number of parameters must be multiple of 2, got %v", pairs) 303 | } 304 | m := make(map[string]string, length/2) 305 | for i := 0; i < length; i += 2 { 306 | m[pairs[i]] = pairs[i+1] 307 | } 308 | return m, nil 309 | } 310 | 311 | // matchInArray returns true if the given string value is in the array. 312 | func matchInArray(arr []string, value string) bool { 313 | for _, v := range arr { 314 | if v == value { 315 | return true 316 | } 317 | } 318 | return false 319 | } 320 | 321 | // matchMap returns true if the given key/value pairs exist in a given map. 322 | func matchMap(toCheck map[string]string, toMatch map[string][]string, 323 | canonicalKey bool) bool { 324 | for k, v := range toCheck { 325 | // Check if key exists. 326 | if canonicalKey { 327 | k = http.CanonicalHeaderKey(k) 328 | } 329 | if values := toMatch[k]; values == nil { 330 | return false 331 | } else if v != "" { 332 | // If value was defined as an empty string we only check that the 333 | // key exists. Otherwise we also check for equality. 334 | valueExists := false 335 | for _, value := range values { 336 | if v == value { 337 | valueExists = true 338 | break 339 | } 340 | } 341 | if !valueExists { 342 | return false 343 | } 344 | } 345 | } 346 | return true 347 | } 348 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /third_party/github.com/gorilla/mux/route.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | ) 14 | 15 | // Route stores information to match a request and build URLs. 16 | type Route struct { 17 | // Parent where the route was registered (a Router). 18 | parent parentRoute 19 | // Request handler for the route. 20 | handler http.Handler 21 | // List of matchers. 22 | matchers []matcher 23 | // Manager for the variables from host and path. 24 | regexp *routeRegexpGroup 25 | // If true, when the path pattern is "/path/", accessing "/path" will 26 | // redirect to the former and vice versa. 27 | strictSlash bool 28 | // If true, this route never matches: it is only used to build URLs. 29 | buildOnly bool 30 | // The name used to build URLs. 31 | name string 32 | // Error resulted from building a route. 33 | err error 34 | } 35 | 36 | // Match matches the route against the request. 37 | func (r *Route) Match(req *http.Request, match *RouteMatch) bool { 38 | if r.buildOnly || r.err != nil { 39 | return false 40 | } 41 | // Match everything. 42 | for _, m := range r.matchers { 43 | if matched := m.Match(req, match); !matched { 44 | return false 45 | } 46 | } 47 | // Yay, we have a match. Let's collect some info about it. 48 | if match.Route == nil { 49 | match.Route = r 50 | } 51 | if match.Handler == nil { 52 | match.Handler = r.handler 53 | } 54 | if match.Vars == nil { 55 | match.Vars = make(map[string]string) 56 | } 57 | // Set variables. 58 | if r.regexp != nil { 59 | r.regexp.setMatch(req, match, r) 60 | } 61 | return true 62 | } 63 | 64 | // ---------------------------------------------------------------------------- 65 | // Route attributes 66 | // ---------------------------------------------------------------------------- 67 | 68 | // GetError returns an error resulted from building the route, if any. 69 | func (r *Route) GetError() error { 70 | return r.err 71 | } 72 | 73 | // BuildOnly sets the route to never match: it is only used to build URLs. 74 | func (r *Route) BuildOnly() *Route { 75 | r.buildOnly = true 76 | return r 77 | } 78 | 79 | // Handler -------------------------------------------------------------------- 80 | 81 | // Handler sets a handler for the route. 82 | func (r *Route) Handler(handler http.Handler) *Route { 83 | if r.err == nil { 84 | r.handler = handler 85 | } 86 | return r 87 | } 88 | 89 | // HandlerFunc sets a handler function for the route. 90 | func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route { 91 | return r.Handler(http.HandlerFunc(f)) 92 | } 93 | 94 | // GetHandler returns the handler for the route, if any. 95 | func (r *Route) GetHandler() http.Handler { 96 | return r.handler 97 | } 98 | 99 | // Name ----------------------------------------------------------------------- 100 | 101 | // Name sets the name for the route, used to build URLs. 102 | // If the name was registered already it will be overwritten. 103 | func (r *Route) Name(name string) *Route { 104 | if r.name != "" { 105 | r.err = fmt.Errorf("mux: route already has name %q, can't set %q", 106 | r.name, name) 107 | } 108 | if r.err == nil { 109 | r.name = name 110 | r.getNamedRoutes()[name] = r 111 | } 112 | return r 113 | } 114 | 115 | // GetName returns the name for the route, if any. 116 | func (r *Route) GetName() string { 117 | return r.name 118 | } 119 | 120 | // ---------------------------------------------------------------------------- 121 | // Matchers 122 | // ---------------------------------------------------------------------------- 123 | 124 | // matcher types try to match a request. 125 | type matcher interface { 126 | Match(*http.Request, *RouteMatch) bool 127 | } 128 | 129 | // addMatcher adds a matcher to the route. 130 | func (r *Route) addMatcher(m matcher) *Route { 131 | if r.err == nil { 132 | r.matchers = append(r.matchers, m) 133 | } 134 | return r 135 | } 136 | 137 | // addRegexpMatcher adds a host or path matcher and builder to a route. 138 | func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix bool) error { 139 | if r.err != nil { 140 | return r.err 141 | } 142 | r.regexp = r.getRegexpGroup() 143 | if !matchHost { 144 | if len(tpl) == 0 || tpl[0] != '/' { 145 | return fmt.Errorf("mux: path must start with a slash, got %q", tpl) 146 | } 147 | if r.regexp.path != nil { 148 | tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl 149 | } 150 | } 151 | rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, r.strictSlash) 152 | if err != nil { 153 | return err 154 | } 155 | if matchHost { 156 | if r.regexp.path != nil { 157 | if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { 158 | return err 159 | } 160 | } 161 | r.regexp.host = rr 162 | } else { 163 | if r.regexp.host != nil { 164 | if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil { 165 | return err 166 | } 167 | } 168 | r.regexp.path = rr 169 | } 170 | r.addMatcher(rr) 171 | return nil 172 | } 173 | 174 | // Headers -------------------------------------------------------------------- 175 | 176 | // headerMatcher matches the request against header values. 177 | type headerMatcher map[string]string 178 | 179 | func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { 180 | return matchMap(m, r.Header, true) 181 | } 182 | 183 | // Headers adds a matcher for request header values. 184 | // It accepts a sequence of key/value pairs to be matched. For example: 185 | // 186 | // r := mux.NewRouter() 187 | // r.Headers("Content-Type", "application/json", 188 | // "X-Requested-With", "XMLHttpRequest") 189 | // 190 | // The above route will only match if both request header values match. 191 | // 192 | // It the value is an empty string, it will match any value if the key is set. 193 | func (r *Route) Headers(pairs ...string) *Route { 194 | if r.err == nil { 195 | var headers map[string]string 196 | headers, r.err = mapFromPairs(pairs...) 197 | return r.addMatcher(headerMatcher(headers)) 198 | } 199 | return r 200 | } 201 | 202 | // Host ----------------------------------------------------------------------- 203 | 204 | // Host adds a matcher for the URL host. 205 | // It accepts a template with zero or more URL variables enclosed by {}. 206 | // Variables can define an optional regexp pattern to me matched: 207 | // 208 | // - {name} matches anything until the next dot. 209 | // 210 | // - {name:pattern} matches the given regexp pattern. 211 | // 212 | // For example: 213 | // 214 | // r := mux.NewRouter() 215 | // r.Host("www.domain.com") 216 | // r.Host("{subdomain}.domain.com") 217 | // r.Host("{subdomain:[a-z]+}.domain.com") 218 | // 219 | // Variable names must be unique in a given route. They can be retrieved 220 | // calling mux.Vars(request). 221 | func (r *Route) Host(tpl string) *Route { 222 | r.err = r.addRegexpMatcher(tpl, true, false) 223 | return r 224 | } 225 | 226 | // MatcherFunc ---------------------------------------------------------------- 227 | 228 | // MatcherFunc is the function signature used by custom matchers. 229 | type MatcherFunc func(*http.Request, *RouteMatch) bool 230 | 231 | func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { 232 | return m(r, match) 233 | } 234 | 235 | // MatcherFunc adds a custom function to be used as request matcher. 236 | func (r *Route) MatcherFunc(f MatcherFunc) *Route { 237 | return r.addMatcher(f) 238 | } 239 | 240 | // Methods -------------------------------------------------------------------- 241 | 242 | // methodMatcher matches the request against HTTP methods. 243 | type methodMatcher []string 244 | 245 | func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { 246 | return matchInArray(m, r.Method) 247 | } 248 | 249 | // Methods adds a matcher for HTTP methods. 250 | // It accepts a sequence of one or more methods to be matched, e.g.: 251 | // "GET", "POST", "PUT". 252 | func (r *Route) Methods(methods ...string) *Route { 253 | for k, v := range methods { 254 | methods[k] = strings.ToUpper(v) 255 | } 256 | return r.addMatcher(methodMatcher(methods)) 257 | } 258 | 259 | // Path ----------------------------------------------------------------------- 260 | 261 | // Path adds a matcher for the URL path. 262 | // It accepts a template with zero or more URL variables enclosed by {}. 263 | // Variables can define an optional regexp pattern to me matched: 264 | // 265 | // - {name} matches anything until the next slash. 266 | // 267 | // - {name:pattern} matches the given regexp pattern. 268 | // 269 | // For example: 270 | // 271 | // r := mux.NewRouter() 272 | // r.Path("/products/").Handler(ProductsHandler) 273 | // r.Path("/products/{key}").Handler(ProductsHandler) 274 | // r.Path("/articles/{category}/{id:[0-9]+}"). 275 | // Handler(ArticleHandler) 276 | // 277 | // Variable names must be unique in a given route. They can be retrieved 278 | // calling mux.Vars(request). 279 | func (r *Route) Path(tpl string) *Route { 280 | r.err = r.addRegexpMatcher(tpl, false, false) 281 | return r 282 | } 283 | 284 | // PathPrefix ----------------------------------------------------------------- 285 | 286 | // PathPrefix adds a matcher for the URL path prefix. 287 | func (r *Route) PathPrefix(tpl string) *Route { 288 | r.strictSlash = false 289 | r.err = r.addRegexpMatcher(tpl, false, true) 290 | return r 291 | } 292 | 293 | // Query ---------------------------------------------------------------------- 294 | 295 | // queryMatcher matches the request against URL queries. 296 | type queryMatcher map[string]string 297 | 298 | func (m queryMatcher) Match(r *http.Request, match *RouteMatch) bool { 299 | return matchMap(m, r.URL.Query(), false) 300 | } 301 | 302 | // Queries adds a matcher for URL query values. 303 | // It accepts a sequence of key/value pairs. For example: 304 | // 305 | // r := mux.NewRouter() 306 | // r.Queries("foo", "bar", "baz", "ding") 307 | // 308 | // The above route will only match if the URL contains the defined queries 309 | // values, e.g.: ?foo=bar&baz=ding. 310 | // 311 | // It the value is an empty string, it will match any value if the key is set. 312 | func (r *Route) Queries(pairs ...string) *Route { 313 | if r.err == nil { 314 | var queries map[string]string 315 | queries, r.err = mapFromPairs(pairs...) 316 | return r.addMatcher(queryMatcher(queries)) 317 | } 318 | return r 319 | } 320 | 321 | // Schemes -------------------------------------------------------------------- 322 | 323 | // schemeMatcher matches the request against URL schemes. 324 | type schemeMatcher []string 325 | 326 | func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { 327 | return matchInArray(m, r.URL.Scheme) 328 | } 329 | 330 | // Schemes adds a matcher for URL schemes. 331 | // It accepts a sequence of schemes to be matched, e.g.: "http", "https". 332 | func (r *Route) Schemes(schemes ...string) *Route { 333 | for k, v := range schemes { 334 | schemes[k] = strings.ToLower(v) 335 | } 336 | return r.addMatcher(schemeMatcher(schemes)) 337 | } 338 | 339 | // Subrouter ------------------------------------------------------------------ 340 | 341 | // Subrouter creates a subrouter for the route. 342 | // 343 | // It will test the inner routes only if the parent route matched. For example: 344 | // 345 | // r := mux.NewRouter() 346 | // s := r.Host("www.domain.com").Subrouter() 347 | // s.HandleFunc("/products/", ProductsHandler) 348 | // s.HandleFunc("/products/{key}", ProductHandler) 349 | // s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) 350 | // 351 | // Here, the routes registered in the subrouter won't be tested if the host 352 | // doesn't match. 353 | func (r *Route) Subrouter() *Router { 354 | router := &Router{parent: r, strictSlash: r.strictSlash} 355 | r.addMatcher(router) 356 | return router 357 | } 358 | 359 | // ---------------------------------------------------------------------------- 360 | // URL building 361 | // ---------------------------------------------------------------------------- 362 | 363 | // URL builds a URL for the route. 364 | // 365 | // It accepts a sequence of key/value pairs for the route variables. For 366 | // example, given this route: 367 | // 368 | // r := mux.NewRouter() 369 | // r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 370 | // Name("article") 371 | // 372 | // ...a URL for it can be built using: 373 | // 374 | // url, err := r.Get("article").URL("category", "technology", "id", "42") 375 | // 376 | // ...which will return an url.URL with the following path: 377 | // 378 | // "/articles/technology/42" 379 | // 380 | // This also works for host variables: 381 | // 382 | // r := mux.NewRouter() 383 | // r.Host("{subdomain}.domain.com"). 384 | // HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). 385 | // Name("article") 386 | // 387 | // // url.String() will be "http://news.domain.com/articles/technology/42" 388 | // url, err := r.Get("article").URL("subdomain", "news", 389 | // "category", "technology", 390 | // "id", "42") 391 | // 392 | // All variables defined in the route are required, and their values must 393 | // conform to the corresponding patterns. 394 | func (r *Route) URL(pairs ...string) (*url.URL, error) { 395 | if r.err != nil { 396 | return nil, r.err 397 | } 398 | if r.regexp == nil { 399 | return nil, errors.New("mux: route doesn't have a host or path") 400 | } 401 | var scheme, host, path string 402 | var err error 403 | if r.regexp.host != nil { 404 | // Set a default scheme. 405 | scheme = "http" 406 | if host, err = r.regexp.host.url(pairs...); err != nil { 407 | return nil, err 408 | } 409 | } 410 | if r.regexp.path != nil { 411 | if path, err = r.regexp.path.url(pairs...); err != nil { 412 | return nil, err 413 | } 414 | } 415 | return &url.URL{ 416 | Scheme: scheme, 417 | Host: host, 418 | Path: path, 419 | }, nil 420 | } 421 | 422 | // URLHost builds the host part of the URL for a route. See Route.URL(). 423 | // 424 | // The route must have a host defined. 425 | func (r *Route) URLHost(pairs ...string) (*url.URL, error) { 426 | if r.err != nil { 427 | return nil, r.err 428 | } 429 | if r.regexp == nil || r.regexp.host == nil { 430 | return nil, errors.New("mux: route doesn't have a host") 431 | } 432 | host, err := r.regexp.host.url(pairs...) 433 | if err != nil { 434 | return nil, err 435 | } 436 | return &url.URL{ 437 | Scheme: "http", 438 | Host: host, 439 | }, nil 440 | } 441 | 442 | // URLPath builds the path part of the URL for a route. See Route.URL(). 443 | // 444 | // The route must have a path defined. 445 | func (r *Route) URLPath(pairs ...string) (*url.URL, error) { 446 | if r.err != nil { 447 | return nil, r.err 448 | } 449 | if r.regexp == nil || r.regexp.path == nil { 450 | return nil, errors.New("mux: route doesn't have a path") 451 | } 452 | path, err := r.regexp.path.url(pairs...) 453 | if err != nil { 454 | return nil, err 455 | } 456 | return &url.URL{ 457 | Path: path, 458 | }, nil 459 | } 460 | 461 | // ---------------------------------------------------------------------------- 462 | // parentRoute 463 | // ---------------------------------------------------------------------------- 464 | 465 | // parentRoute allows routes to know about parent host and path definitions. 466 | type parentRoute interface { 467 | getNamedRoutes() map[string]*Route 468 | getRegexpGroup() *routeRegexpGroup 469 | } 470 | 471 | // getNamedRoutes returns the map where named routes are registered. 472 | func (r *Route) getNamedRoutes() map[string]*Route { 473 | if r.parent == nil { 474 | // During tests router is not always set. 475 | r.parent = NewRouter() 476 | } 477 | return r.parent.getNamedRoutes() 478 | } 479 | 480 | // getRegexpGroup returns regexp definitions from this route. 481 | func (r *Route) getRegexpGroup() *routeRegexpGroup { 482 | if r.regexp == nil { 483 | if r.parent == nil { 484 | // During tests router is not always set. 485 | r.parent = NewRouter() 486 | } 487 | regexp := r.parent.getRegexpGroup() 488 | if regexp == nil { 489 | r.regexp = new(routeRegexpGroup) 490 | } else { 491 | // Copy. 492 | r.regexp = &routeRegexpGroup{ 493 | host: regexp.host, 494 | path: regexp.path, 495 | } 496 | } 497 | } 498 | return r.regexp 499 | } 500 | -------------------------------------------------------------------------------- /third_party/github.com/gorilla/mux/old_test.go: -------------------------------------------------------------------------------- 1 | // Old tests ported to Go1. This is a mess. Want to drop it one day. 2 | 3 | // Copyright 2011 Gorilla Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | package mux 8 | 9 | import ( 10 | "bytes" 11 | "net/http" 12 | "testing" 13 | ) 14 | 15 | // ---------------------------------------------------------------------------- 16 | // ResponseRecorder 17 | // ---------------------------------------------------------------------------- 18 | // Copyright 2009 The Go Authors. All rights reserved. 19 | // Use of this source code is governed by a BSD-style 20 | // license that can be found in the LICENSE file. 21 | 22 | // ResponseRecorder is an implementation of http.ResponseWriter that 23 | // records its mutations for later inspection in tests. 24 | type ResponseRecorder struct { 25 | Code int // the HTTP response code from WriteHeader 26 | HeaderMap http.Header // the HTTP response headers 27 | Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to 28 | Flushed bool 29 | } 30 | 31 | // NewRecorder returns an initialized ResponseRecorder. 32 | func NewRecorder() *ResponseRecorder { 33 | return &ResponseRecorder{ 34 | HeaderMap: make(http.Header), 35 | Body: new(bytes.Buffer), 36 | } 37 | } 38 | 39 | // DefaultRemoteAddr is the default remote address to return in RemoteAddr if 40 | // an explicit DefaultRemoteAddr isn't set on ResponseRecorder. 41 | const DefaultRemoteAddr = "1.2.3.4" 42 | 43 | // Header returns the response headers. 44 | func (rw *ResponseRecorder) Header() http.Header { 45 | return rw.HeaderMap 46 | } 47 | 48 | // Write always succeeds and writes to rw.Body, if not nil. 49 | func (rw *ResponseRecorder) Write(buf []byte) (int, error) { 50 | if rw.Body != nil { 51 | rw.Body.Write(buf) 52 | } 53 | if rw.Code == 0 { 54 | rw.Code = http.StatusOK 55 | } 56 | return len(buf), nil 57 | } 58 | 59 | // WriteHeader sets rw.Code. 60 | func (rw *ResponseRecorder) WriteHeader(code int) { 61 | rw.Code = code 62 | } 63 | 64 | // Flush sets rw.Flushed to true. 65 | func (rw *ResponseRecorder) Flush() { 66 | rw.Flushed = true 67 | } 68 | 69 | // ---------------------------------------------------------------------------- 70 | 71 | func TestRouteMatchers(t *testing.T) { 72 | var scheme, host, path, query, method string 73 | var headers map[string]string 74 | var resultVars map[bool]map[string]string 75 | 76 | router := NewRouter() 77 | router.NewRoute().Host("{var1}.google.com"). 78 | Path("/{var2:[a-z]+}/{var3:[0-9]+}"). 79 | Queries("foo", "bar"). 80 | Methods("GET"). 81 | Schemes("https"). 82 | Headers("x-requested-with", "XMLHttpRequest") 83 | router.NewRoute().Host("www.{var4}.com"). 84 | PathPrefix("/foo/{var5:[a-z]+}/{var6:[0-9]+}"). 85 | Queries("baz", "ding"). 86 | Methods("POST"). 87 | Schemes("http"). 88 | Headers("Content-Type", "application/json") 89 | 90 | reset := func() { 91 | // Everything match. 92 | scheme = "https" 93 | host = "www.google.com" 94 | path = "/product/42" 95 | query = "?foo=bar" 96 | method = "GET" 97 | headers = map[string]string{"X-Requested-With": "XMLHttpRequest"} 98 | resultVars = map[bool]map[string]string{ 99 | true: {"var1": "www", "var2": "product", "var3": "42"}, 100 | false: {}, 101 | } 102 | } 103 | 104 | reset2 := func() { 105 | // Everything match. 106 | scheme = "http" 107 | host = "www.google.com" 108 | path = "/foo/product/42/path/that/is/ignored" 109 | query = "?baz=ding" 110 | method = "POST" 111 | headers = map[string]string{"Content-Type": "application/json"} 112 | resultVars = map[bool]map[string]string{ 113 | true: {"var4": "google", "var5": "product", "var6": "42"}, 114 | false: {}, 115 | } 116 | } 117 | 118 | match := func(shouldMatch bool) { 119 | url := scheme + "://" + host + path + query 120 | request, _ := http.NewRequest(method, url, nil) 121 | for key, value := range headers { 122 | request.Header.Add(key, value) 123 | } 124 | 125 | var routeMatch RouteMatch 126 | matched := router.Match(request, &routeMatch) 127 | if matched != shouldMatch { 128 | // Need better messages. :) 129 | if matched { 130 | t.Errorf("Should match.") 131 | } else { 132 | t.Errorf("Should not match.") 133 | } 134 | } 135 | 136 | if matched { 137 | currentRoute := routeMatch.Route 138 | if currentRoute == nil { 139 | t.Errorf("Expected a current route.") 140 | } 141 | vars := routeMatch.Vars 142 | expectedVars := resultVars[shouldMatch] 143 | if len(vars) != len(expectedVars) { 144 | t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars) 145 | } 146 | for name, value := range vars { 147 | if expectedVars[name] != value { 148 | t.Errorf("Expected vars: %v Got: %v.", expectedVars, vars) 149 | } 150 | } 151 | } 152 | } 153 | 154 | // 1st route -------------------------------------------------------------- 155 | 156 | // Everything match. 157 | reset() 158 | match(true) 159 | 160 | // Scheme doesn't match. 161 | reset() 162 | scheme = "http" 163 | match(false) 164 | 165 | // Host doesn't match. 166 | reset() 167 | host = "www.mygoogle.com" 168 | match(false) 169 | 170 | // Path doesn't match. 171 | reset() 172 | path = "/product/notdigits" 173 | match(false) 174 | 175 | // Query doesn't match. 176 | reset() 177 | query = "?foo=baz" 178 | match(false) 179 | 180 | // Method doesn't match. 181 | reset() 182 | method = "POST" 183 | match(false) 184 | 185 | // Header doesn't match. 186 | reset() 187 | headers = map[string]string{} 188 | match(false) 189 | 190 | // Everything match, again. 191 | reset() 192 | match(true) 193 | 194 | // 2nd route -------------------------------------------------------------- 195 | 196 | // Everything match. 197 | reset2() 198 | match(true) 199 | 200 | // Scheme doesn't match. 201 | reset2() 202 | scheme = "https" 203 | match(false) 204 | 205 | // Host doesn't match. 206 | reset2() 207 | host = "sub.google.com" 208 | match(false) 209 | 210 | // Path doesn't match. 211 | reset2() 212 | path = "/bar/product/42" 213 | match(false) 214 | 215 | // Query doesn't match. 216 | reset2() 217 | query = "?foo=baz" 218 | match(false) 219 | 220 | // Method doesn't match. 221 | reset2() 222 | method = "GET" 223 | match(false) 224 | 225 | // Header doesn't match. 226 | reset2() 227 | headers = map[string]string{} 228 | match(false) 229 | 230 | // Everything match, again. 231 | reset2() 232 | match(true) 233 | } 234 | 235 | type headerMatcherTest struct { 236 | matcher headerMatcher 237 | headers map[string]string 238 | result bool 239 | } 240 | 241 | var headerMatcherTests = []headerMatcherTest{ 242 | { 243 | matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}), 244 | headers: map[string]string{"X-Requested-With": "XMLHttpRequest"}, 245 | result: true, 246 | }, 247 | { 248 | matcher: headerMatcher(map[string]string{"x-requested-with": ""}), 249 | headers: map[string]string{"X-Requested-With": "anything"}, 250 | result: true, 251 | }, 252 | { 253 | matcher: headerMatcher(map[string]string{"x-requested-with": "XMLHttpRequest"}), 254 | headers: map[string]string{}, 255 | result: false, 256 | }, 257 | } 258 | 259 | type hostMatcherTest struct { 260 | matcher *Route 261 | url string 262 | vars map[string]string 263 | result bool 264 | } 265 | 266 | var hostMatcherTests = []hostMatcherTest{ 267 | { 268 | matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), 269 | url: "http://abc.def.ghi/", 270 | vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, 271 | result: true, 272 | }, 273 | { 274 | matcher: NewRouter().NewRoute().Host("{foo:[a-z][a-z][a-z]}.{bar:[a-z][a-z][a-z]}.{baz:[a-z][a-z][a-z]}"), 275 | url: "http://a.b.c/", 276 | vars: map[string]string{"foo": "abc", "bar": "def", "baz": "ghi"}, 277 | result: false, 278 | }, 279 | } 280 | 281 | type methodMatcherTest struct { 282 | matcher methodMatcher 283 | method string 284 | result bool 285 | } 286 | 287 | var methodMatcherTests = []methodMatcherTest{ 288 | { 289 | matcher: methodMatcher([]string{"GET", "POST", "PUT"}), 290 | method: "GET", 291 | result: true, 292 | }, 293 | { 294 | matcher: methodMatcher([]string{"GET", "POST", "PUT"}), 295 | method: "POST", 296 | result: true, 297 | }, 298 | { 299 | matcher: methodMatcher([]string{"GET", "POST", "PUT"}), 300 | method: "PUT", 301 | result: true, 302 | }, 303 | { 304 | matcher: methodMatcher([]string{"GET", "POST", "PUT"}), 305 | method: "DELETE", 306 | result: false, 307 | }, 308 | } 309 | 310 | type pathMatcherTest struct { 311 | matcher *Route 312 | url string 313 | vars map[string]string 314 | result bool 315 | } 316 | 317 | var pathMatcherTests = []pathMatcherTest{ 318 | { 319 | matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"), 320 | url: "http://localhost:8080/123/456/789", 321 | vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"}, 322 | result: true, 323 | }, 324 | { 325 | matcher: NewRouter().NewRoute().Path("/{foo:[0-9][0-9][0-9]}/{bar:[0-9][0-9][0-9]}/{baz:[0-9][0-9][0-9]}"), 326 | url: "http://localhost:8080/1/2/3", 327 | vars: map[string]string{"foo": "123", "bar": "456", "baz": "789"}, 328 | result: false, 329 | }, 330 | } 331 | 332 | type queryMatcherTest struct { 333 | matcher queryMatcher 334 | url string 335 | result bool 336 | } 337 | 338 | var queryMatcherTests = []queryMatcherTest{ 339 | { 340 | matcher: queryMatcher(map[string]string{"foo": "bar", "baz": "ding"}), 341 | url: "http://localhost:8080/?foo=bar&baz=ding", 342 | result: true, 343 | }, 344 | { 345 | matcher: queryMatcher(map[string]string{"foo": "", "baz": ""}), 346 | url: "http://localhost:8080/?foo=anything&baz=anything", 347 | result: true, 348 | }, 349 | { 350 | matcher: queryMatcher(map[string]string{"foo": "ding", "baz": "bar"}), 351 | url: "http://localhost:8080/?foo=bar&baz=ding", 352 | result: false, 353 | }, 354 | { 355 | matcher: queryMatcher(map[string]string{"bar": "foo", "ding": "baz"}), 356 | url: "http://localhost:8080/?foo=bar&baz=ding", 357 | result: false, 358 | }, 359 | } 360 | 361 | type schemeMatcherTest struct { 362 | matcher schemeMatcher 363 | url string 364 | result bool 365 | } 366 | 367 | var schemeMatcherTests = []schemeMatcherTest{ 368 | { 369 | matcher: schemeMatcher([]string{"http", "https"}), 370 | url: "http://localhost:8080/", 371 | result: true, 372 | }, 373 | { 374 | matcher: schemeMatcher([]string{"http", "https"}), 375 | url: "https://localhost:8080/", 376 | result: true, 377 | }, 378 | { 379 | matcher: schemeMatcher([]string{"https"}), 380 | url: "http://localhost:8080/", 381 | result: false, 382 | }, 383 | { 384 | matcher: schemeMatcher([]string{"http"}), 385 | url: "https://localhost:8080/", 386 | result: false, 387 | }, 388 | } 389 | 390 | type urlBuildingTest struct { 391 | route *Route 392 | vars []string 393 | url string 394 | } 395 | 396 | var urlBuildingTests = []urlBuildingTest{ 397 | { 398 | route: new(Route).Host("foo.domain.com"), 399 | vars: []string{}, 400 | url: "http://foo.domain.com", 401 | }, 402 | { 403 | route: new(Route).Host("{subdomain}.domain.com"), 404 | vars: []string{"subdomain", "bar"}, 405 | url: "http://bar.domain.com", 406 | }, 407 | { 408 | route: new(Route).Host("foo.domain.com").Path("/articles"), 409 | vars: []string{}, 410 | url: "http://foo.domain.com/articles", 411 | }, 412 | { 413 | route: new(Route).Path("/articles"), 414 | vars: []string{}, 415 | url: "/articles", 416 | }, 417 | { 418 | route: new(Route).Path("/articles/{category}/{id:[0-9]+}"), 419 | vars: []string{"category", "technology", "id", "42"}, 420 | url: "/articles/technology/42", 421 | }, 422 | { 423 | route: new(Route).Host("{subdomain}.domain.com").Path("/articles/{category}/{id:[0-9]+}"), 424 | vars: []string{"subdomain", "foo", "category", "technology", "id", "42"}, 425 | url: "http://foo.domain.com/articles/technology/42", 426 | }, 427 | } 428 | 429 | func TestHeaderMatcher(t *testing.T) { 430 | for _, v := range headerMatcherTests { 431 | request, _ := http.NewRequest("GET", "http://localhost:8080/", nil) 432 | for key, value := range v.headers { 433 | request.Header.Add(key, value) 434 | } 435 | var routeMatch RouteMatch 436 | result := v.matcher.Match(request, &routeMatch) 437 | if result != v.result { 438 | if v.result { 439 | t.Errorf("%#v: should match %v.", v.matcher, request.Header) 440 | } else { 441 | t.Errorf("%#v: should not match %v.", v.matcher, request.Header) 442 | } 443 | } 444 | } 445 | } 446 | 447 | func TestHostMatcher(t *testing.T) { 448 | for _, v := range hostMatcherTests { 449 | request, _ := http.NewRequest("GET", v.url, nil) 450 | var routeMatch RouteMatch 451 | result := v.matcher.Match(request, &routeMatch) 452 | vars := routeMatch.Vars 453 | if result != v.result { 454 | if v.result { 455 | t.Errorf("%#v: should match %v.", v.matcher, v.url) 456 | } else { 457 | t.Errorf("%#v: should not match %v.", v.matcher, v.url) 458 | } 459 | } 460 | if result { 461 | if len(vars) != len(v.vars) { 462 | t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars)) 463 | } 464 | for name, value := range vars { 465 | if v.vars[name] != value { 466 | t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value) 467 | } 468 | } 469 | } else { 470 | if len(vars) != 0 { 471 | t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars)) 472 | } 473 | } 474 | } 475 | } 476 | 477 | func TestMethodMatcher(t *testing.T) { 478 | for _, v := range methodMatcherTests { 479 | request, _ := http.NewRequest(v.method, "http://localhost:8080/", nil) 480 | var routeMatch RouteMatch 481 | result := v.matcher.Match(request, &routeMatch) 482 | if result != v.result { 483 | if v.result { 484 | t.Errorf("%#v: should match %v.", v.matcher, v.method) 485 | } else { 486 | t.Errorf("%#v: should not match %v.", v.matcher, v.method) 487 | } 488 | } 489 | } 490 | } 491 | 492 | func TestPathMatcher(t *testing.T) { 493 | for _, v := range pathMatcherTests { 494 | request, _ := http.NewRequest("GET", v.url, nil) 495 | var routeMatch RouteMatch 496 | result := v.matcher.Match(request, &routeMatch) 497 | vars := routeMatch.Vars 498 | if result != v.result { 499 | if v.result { 500 | t.Errorf("%#v: should match %v.", v.matcher, v.url) 501 | } else { 502 | t.Errorf("%#v: should not match %v.", v.matcher, v.url) 503 | } 504 | } 505 | if result { 506 | if len(vars) != len(v.vars) { 507 | t.Errorf("%#v: vars length should be %v, got %v.", v.matcher, len(v.vars), len(vars)) 508 | } 509 | for name, value := range vars { 510 | if v.vars[name] != value { 511 | t.Errorf("%#v: expected value %v for key %v, got %v.", v.matcher, v.vars[name], name, value) 512 | } 513 | } 514 | } else { 515 | if len(vars) != 0 { 516 | t.Errorf("%#v: vars length should be 0, got %v.", v.matcher, len(vars)) 517 | } 518 | } 519 | } 520 | } 521 | 522 | func TestQueryMatcher(t *testing.T) { 523 | for _, v := range queryMatcherTests { 524 | request, _ := http.NewRequest("GET", v.url, nil) 525 | var routeMatch RouteMatch 526 | result := v.matcher.Match(request, &routeMatch) 527 | if result != v.result { 528 | if v.result { 529 | t.Errorf("%#v: should match %v.", v.matcher, v.url) 530 | } else { 531 | t.Errorf("%#v: should not match %v.", v.matcher, v.url) 532 | } 533 | } 534 | } 535 | } 536 | 537 | func TestSchemeMatcher(t *testing.T) { 538 | for _, v := range queryMatcherTests { 539 | request, _ := http.NewRequest("GET", v.url, nil) 540 | var routeMatch RouteMatch 541 | result := v.matcher.Match(request, &routeMatch) 542 | if result != v.result { 543 | if v.result { 544 | t.Errorf("%#v: should match %v.", v.matcher, v.url) 545 | } else { 546 | t.Errorf("%#v: should not match %v.", v.matcher, v.url) 547 | } 548 | } 549 | } 550 | } 551 | 552 | func TestUrlBuilding(t *testing.T) { 553 | 554 | for _, v := range urlBuildingTests { 555 | u, _ := v.route.URL(v.vars...) 556 | url := u.String() 557 | if url != v.url { 558 | t.Errorf("expected %v, got %v", v.url, url) 559 | /* 560 | reversePath := "" 561 | reverseHost := "" 562 | if v.route.pathTemplate != nil { 563 | reversePath = v.route.pathTemplate.Reverse 564 | } 565 | if v.route.hostTemplate != nil { 566 | reverseHost = v.route.hostTemplate.Reverse 567 | } 568 | 569 | t.Errorf("%#v:\nexpected: %q\ngot: %q\nreverse path: %q\nreverse host: %q", v.route, v.url, url, reversePath, reverseHost) 570 | */ 571 | } 572 | } 573 | 574 | ArticleHandler := func(w http.ResponseWriter, r *http.Request) { 575 | } 576 | 577 | router := NewRouter() 578 | router.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler).Name("article") 579 | 580 | url, _ := router.Get("article").URL("category", "technology", "id", "42") 581 | expected := "/articles/technology/42" 582 | if url.String() != expected { 583 | t.Errorf("Expected %v, got %v", expected, url.String()) 584 | } 585 | } 586 | 587 | func TestMatchedRouteName(t *testing.T) { 588 | routeName := "stock" 589 | router := NewRouter() 590 | route := router.NewRoute().Path("/products/").Name(routeName) 591 | 592 | url := "http://www.domain.com/products/" 593 | request, _ := http.NewRequest("GET", url, nil) 594 | var rv RouteMatch 595 | ok := router.Match(request, &rv) 596 | 597 | if !ok || rv.Route != route { 598 | t.Errorf("Expected same route, got %+v.", rv.Route) 599 | } 600 | 601 | retName := rv.Route.GetName() 602 | if retName != routeName { 603 | t.Errorf("Expected %q, got %q.", routeName, retName) 604 | } 605 | } 606 | 607 | func TestSubRouting(t *testing.T) { 608 | // Example from docs. 609 | router := NewRouter() 610 | subrouter := router.NewRoute().Host("www.domain.com").Subrouter() 611 | route := subrouter.NewRoute().Path("/products/").Name("products") 612 | 613 | url := "http://www.domain.com/products/" 614 | request, _ := http.NewRequest("GET", url, nil) 615 | var rv RouteMatch 616 | ok := router.Match(request, &rv) 617 | 618 | if !ok || rv.Route != route { 619 | t.Errorf("Expected same route, got %+v.", rv.Route) 620 | } 621 | 622 | u, _ := router.Get("products").URL() 623 | builtUrl := u.String() 624 | // Yay, subroute aware of the domain when building! 625 | if builtUrl != url { 626 | t.Errorf("Expected %q, got %q.", url, builtUrl) 627 | } 628 | } 629 | 630 | func TestVariableNames(t *testing.T) { 631 | route := new(Route).Host("{arg1}.domain.com").Path("/{arg1}/{arg2:[0-9]+}") 632 | if route.err == nil { 633 | t.Errorf("Expected error for duplicated variable names") 634 | } 635 | } 636 | 637 | func TestRedirectSlash(t *testing.T) { 638 | var route *Route 639 | var routeMatch RouteMatch 640 | r := NewRouter() 641 | 642 | r.StrictSlash(false) 643 | route = r.NewRoute() 644 | if route.strictSlash != false { 645 | t.Errorf("Expected false redirectSlash.") 646 | } 647 | 648 | r.StrictSlash(true) 649 | route = r.NewRoute() 650 | if route.strictSlash != true { 651 | t.Errorf("Expected true redirectSlash.") 652 | } 653 | 654 | route = new(Route) 655 | route.strictSlash = true 656 | route.Path("/{arg1}/{arg2:[0-9]+}/") 657 | request, _ := http.NewRequest("GET", "http://localhost/foo/123", nil) 658 | routeMatch = RouteMatch{} 659 | _ = route.Match(request, &routeMatch) 660 | vars := routeMatch.Vars 661 | if vars["arg1"] != "foo" { 662 | t.Errorf("Expected foo.") 663 | } 664 | if vars["arg2"] != "123" { 665 | t.Errorf("Expected 123.") 666 | } 667 | rsp := NewRecorder() 668 | routeMatch.Handler.ServeHTTP(rsp, request) 669 | if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123/" { 670 | t.Errorf("Expected redirect header.") 671 | } 672 | 673 | route = new(Route) 674 | route.strictSlash = true 675 | route.Path("/{arg1}/{arg2:[0-9]+}") 676 | request, _ = http.NewRequest("GET", "http://localhost/foo/123/", nil) 677 | routeMatch = RouteMatch{} 678 | _ = route.Match(request, &routeMatch) 679 | vars = routeMatch.Vars 680 | if vars["arg1"] != "foo" { 681 | t.Errorf("Expected foo.") 682 | } 683 | if vars["arg2"] != "123" { 684 | t.Errorf("Expected 123.") 685 | } 686 | rsp = NewRecorder() 687 | routeMatch.Handler.ServeHTTP(rsp, request) 688 | if rsp.HeaderMap.Get("Location") != "http://localhost/foo/123" { 689 | t.Errorf("Expected redirect header.") 690 | } 691 | } 692 | 693 | // Test for the new regexp library, still not available in stable Go. 694 | func TestNewRegexp(t *testing.T) { 695 | var p *routeRegexp 696 | var matches []string 697 | 698 | tests := map[string]map[string][]string{ 699 | "/{foo:a{2}}": { 700 | "/a": nil, 701 | "/aa": {"aa"}, 702 | "/aaa": nil, 703 | "/aaaa": nil, 704 | }, 705 | "/{foo:a{2,}}": { 706 | "/a": nil, 707 | "/aa": {"aa"}, 708 | "/aaa": {"aaa"}, 709 | "/aaaa": {"aaaa"}, 710 | }, 711 | "/{foo:a{2,3}}": { 712 | "/a": nil, 713 | "/aa": {"aa"}, 714 | "/aaa": {"aaa"}, 715 | "/aaaa": nil, 716 | }, 717 | "/{foo:[a-z]{3}}/{bar:[a-z]{2}}": { 718 | "/a": nil, 719 | "/ab": nil, 720 | "/abc": nil, 721 | "/abcd": nil, 722 | "/abc/ab": {"abc", "ab"}, 723 | "/abc/abc": nil, 724 | "/abcd/ab": nil, 725 | }, 726 | `/{foo:\w{3,}}/{bar:\d{2,}}`: { 727 | "/a": nil, 728 | "/ab": nil, 729 | "/abc": nil, 730 | "/abc/1": nil, 731 | "/abc/12": {"abc", "12"}, 732 | "/abcd/12": {"abcd", "12"}, 733 | "/abcd/123": {"abcd", "123"}, 734 | }, 735 | } 736 | 737 | for pattern, paths := range tests { 738 | p, _ = newRouteRegexp(pattern, false, false, false) 739 | for path, result := range paths { 740 | matches = p.regexp.FindStringSubmatch(path) 741 | if result == nil { 742 | if matches != nil { 743 | t.Errorf("%v should not match %v.", pattern, path) 744 | } 745 | } else { 746 | if len(matches) != len(result)+1 { 747 | t.Errorf("Expected %v matches, got %v.", len(result)+1, len(matches)) 748 | } else { 749 | for k, v := range result { 750 | if matches[k+1] != v { 751 | t.Errorf("Expected %v, got %v.", v, matches[k+1]) 752 | } 753 | } 754 | } 755 | } 756 | } 757 | } 758 | } 759 | -------------------------------------------------------------------------------- /dashboard/dist/app.js: -------------------------------------------------------------------------------- 1 | angular.module("templates-views",["/page/browser/browser.html","/page/browser/create-node.html","/page/browser/edit-node.html","/page/browser/edit-ttl.html","/page/browser/node-info.html","/page/stats/stats-detail.html","/page/stats/stats.html","/ui/breadcrumb.html","/ui/latency-graph.html","/ui/node-cog.html"]),angular.module("/page/browser/browser.html",[]).run(["$templateCache",function(a){a.put("/page/browser/browser.html",'
\n\n \n \n Create Node\n \n\n
\n
\n\n
\n
\n\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
 KeyValueTTL
\n \n \n {{truncateKey(node.key)}}\n {{truncateKey(node.key)}}\n \n
\n {{node.value || \'--\'}}\n
\n
\n {{node.ttl || \'--\'}}\n
\n\n
\n\n
\n\n
\n
\n\n
\n')}]),angular.module("/page/browser/create-node.html",[]).run(["$templateCache",function(a){a.put("/page/browser/create-node.html",'
\n
\n\n \n\n \n\n \n\n
\n
\n')}]),angular.module("/page/browser/edit-node.html",[]).run(["$templateCache",function(a){a.put("/page/browser/edit-node.html",'
\n
\n\n \n\n \n\n \n\n
\n
\n')}]),angular.module("/page/browser/edit-ttl.html",[]).run(["$templateCache",function(a){a.put("/page/browser/edit-ttl.html",'
\n
\n\n \n\n \n\n \n\n
\n
\n')}]),angular.module("/page/browser/node-info.html",[]).run(["$templateCache",function(a){a.put("/page/browser/node-info.html",'
\n\n \n\n \n\n \n\n
\n')}]),angular.module("/page/stats/stats-detail.html",[]).run(["$templateCache",function(a){a.put("/page/stats/stats-detail.html",'
\n\n \n\n \n\n \n\n
\n')}]),angular.module("/page/stats/stats.html",[]).run(["$templateCache",function(a){a.put("/page/stats/stats.html",'
\n \n
\n\n\n
\n
\n
\n

Peer Latency

\n \n
\n
\n
\n\n\n\n
\n
\n
\n
\n

Leader

\n
\n
Name
\n
\n
Uptime
\n
\n \n
\n\n

Followers

\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
NameLatencyRaft Requests
\n \n \n
\n {{follower.latency.current | number:1 }} ms\n
\n {{follower.counts.fail}} failures,\n {{follower.counts.success}} successes\n
\n\n
\n
\n
\n\n\n\n\n
\n
\n')}]),angular.module("/ui/breadcrumb.html",[]).run(["$templateCache",function(a){a.put("/ui/breadcrumb.html",'\n')}]),angular.module("/ui/latency-graph.html",[]).run(["$templateCache",function(a){a.put("/ui/latency-graph.html",'
\n')}]),angular.module("/ui/node-cog.html",[]).run(["$templateCache",function(a){a.put("/ui/node-cog.html",'
\n \n
\n')}]),angular.module("etcd.module",[]),angular.module("etcd.ui",[]),angular.module("etcd.page",[]);var etcdDashboard=angular.module("etcd.dashboard",["coreos","etcd.module","etcd.ui","etcd.page","ngRoute","ngResource","ngAnimate","ui.bootstrap","templates-views","underscore","jquery","d3"]);etcdDashboard.config(["$routeProvider","$locationProvider","$httpProvider","$compileProvider","pollerSvcProvider","errorMessageSvcProvider","configSvcProvider",function(a,b,c,d,e,f,g){function h(a){return i+a}var i="/mod/dashboard";g.config({siteBasePath:i,libPath:"/mod/dashboard/static/coreos-web"}),b.html5Mode(!0),f.registerFormatter("etcdApi",function(a){return a.data&&a.data.message?a.data.message:"An error occurred."}),c.interceptors.push("interceptorErrorSvc"),e.settings({interval:5e3,maxRetries:5}),a.when(h("/"),{redirectTo:h("/browser")}).when(h("/browser"),{controller:"BrowserCtrl",templateUrl:"/page/browser/browser.html",title:"Key Browser"}).when(h("/stats"),{controller:"StatsCtrl",templateUrl:"/page/stats/stats.html",title:"Stats"}).otherwise({templateUrl:"/404.html",title:"Page Not Found (404)"})}]).run(["$http","$rootScope","$location","$window","$route","_","configSvc","toastSvc","CORE_EVENT",function(a,b,c,d,e,f,g,h,i){b.$on(i.POLL_ERROR,function(){h.error("Error polling for data.")}),b.$on(i.RESP_ERROR,function(a,b){var c="Request Error";b.data&&b.data.message&&(c=b.data.message),h.error(c)}),b.$on(i.PAGE_NOT_FOUND,function(){c.url(g.get().siteBaseUrl+"/404")})}]),angular.module("etcd.module").factory("etcdApiSvc",["$http","$q","$","_","pathSvc",function(a,b,c,d,e){function f(b){var d={ttl:b.ttl};return b.dir?d.dir=!0:d.value=b.value,l().then(function(f){return a({url:f+e.getFullKeyPath(b.key),data:c.param(d),method:"PUT",headers:{"Content-Type":"application/x-www-form-urlencoded"}})})}function g(b){var d={ttl:b.ttl,prevExist:!0};return b.dir?d.dir=!0:d.value=b.value,l().then(function(f){return a({url:f+e.getFullKeyPath(b.key),data:c.param(d),method:"PUT",headers:{"Content-Type":"application/x-www-form-urlencoded"}})})}function h(b){var c;return b.dir&&(c={dir:!0,recursive:!0}),l().then(function(d){return a({url:d+e.getFullKeyPath(b.key),method:"DELETE",params:c,headers:{"Content-Type":"application/x-www-form-urlencoded"}})})}function i(b){return a.get(e.getHost()+e.getFullKeyPath(b),{supressNotifications:!0}).then(function(a){return a.data.node})}function j(b){return a.get(e.getHost()+e.getStatFullKeyPath(b),{supressNotifications:!0})}function k(a){return i("/_etcd/machines/"+a).then(function(a){var b=decodeURIComponent(a.value);return b=b.replace(/&/g,'","').replace(/=/g,'":"'),b=JSON.parse('{"'+b+'"}'),b.etcd})}function l(){return n().then(function(a){return k(a.leaderName)})}function m(b){return k(b).then(function(b){return a.get(b+e.getStatFullKeyPath("self")).then(function(a){return a.data})})}function n(){return j("leader").then(function(a){var b={followers:[],leaderName:a.data.leader};return d.each(a.data.followers,function(a,c){a.name=c,b.followers.push(a)}),b})}return{fetch:i,fetchStat:j,fetchLeaderStats:n,fetchPeerDetailStats:m,getLeaderUri:l,create:f,save:g,"delete":h}}]),angular.module("etcd.module").factory("pathSvc",function(){var a="/v2/keys/",b="/v2/stats/";return{clean:function(a){var b=this.explode(a);return 0===b.length?"":b.join("/")},make:function(a){return"/"+a.join("/")},explode:function(a){var b=a.split("/");return b=b.filter(function(a){return""!==a})},tail:function(a){var b=this.explode(a);return b.length?b[b.length-1]:"/"},truncate:function(a,b){var c="/..";return b=b||10,a&&a.length?a.length<=b?a:c+a.substring(a.length-b+c.length,a.length):""},getFullKeyPath:function(b){var c="/"+this.clean(a+b);return c===a.substring(0,a.length-1)?a:c+"/"},getStatFullKeyPath:function(a){return"/"+this.clean(b+a)},getParent:function(a){var b=a.split("/");return 0===b.length?"/":(b.pop(),"/"+b.join("/"))},getHost:function(){return"http://127.0.0.1:4001"}}}),angular.module("etcd.module").constant("ETCD_EVENT",{NODE_DELETED:"etcd.node_deleted",NODE_CHANGED:"etcd.node_changed"}),angular.module("etcd.page").controller("BrowserCtrl",["$scope","$modal","etcdApiSvc","pathSvc","ETCD_EVENT","d3","pollerSvc",function(a,b,c,d,e,f,g){a.currPath="/",a.currNode=null,a.rowClick=function(b){b.dir&&(a.currPath=b.key)},a.truncateKey=function(a){return d.tail(a)},a.openCreateModal=function(c){b.open({templateUrl:"/page/browser/create-node.html",controller:"CreateNodeCtrl",resolve:{key:f.functor(c||a.currPath)}})},a.$on(e.NODE_CHANGED,function(b,c){var e=d.getParent(c.key);a.refreshNode(e)}),a.$on(e.NODE_DELETED,function(b,c){var e=d.getParent(c.key);a.refreshNode(e)}),a.breadcrumbCallback=function(b){a.currPath=b.path},a.refreshNode=function(b){a.currPath===b?a.fetchNode():a.currPath=b},a.fetchNode=function(){return c.fetch(a.currPath).then(function(b){a.currNode=b})},a.$watch("currPath",function(b){return b&&""!==b?void a.fetchNode():void(a.currPath="/")}),g.register("nodePoller",{fn:a.fetchNode,scope:a})}]),angular.module("etcd.page").controller("CreateNodeCtrl",["$scope","$rootScope","$modalInstance","_","ETCD_EVENT","etcdApiSvc","pathSvc","key",function(a,b,c,d,e,f,g,h){a.key=h,a.keyInputPrefix="/"===h?"/":g.truncate(h,20)+"/",a.save=function(d){a.requestPromise=f.create(d).then(function(){b.$broadcast(e.NODE_CHANGED,d),c.close(d)})},a.cancel=function(){c.dismiss("cancel")}}]),angular.module("etcd.page").controller("CreateNodeFormCtrl",["$scope","pathSvc",function(a,b){a.fields={key:"",value:"",type:"key",ttl:null},a.submit=function(){var c={};c.key=b.clean(a.key+"/"+a.fields.key),c.dir="dir"===a.fields.type,c.ttl=a.fields.ttl?parseInt(a.fields.ttl,10):null,c.dir||(c.value=a.fields.value),a.save(c)}}]),angular.module("etcd.page").controller("EditNodeCtrl",["$scope","$rootScope","$modalInstance","_","ETCD_EVENT","etcdApiSvc","pathSvc","node",function(a,b,c,d,e,f,g,h){a.node=h,a.displayKey=g.truncate(h.key,50)+"/",a.save=function(d){a.requestPromise=f.save(d).then(function(){b.$broadcast(e.NODE_CHANGED,d),c.close(d)})},a.cancel=function(){c.dismiss("cancel")}}]),angular.module("etcd.page").controller("EditNodeFormCtrl",["$scope","pathSvc",function(a){a.fields={value:a.node.value||"",ttl:a.node.ttl||""},a.submit=function(){var b={key:a.node.key,value:a.fields.value,ttl:a.fields.ttl?parseInt(a.fields.ttl,10):null};a.save(b)}}]),angular.module("etcd.page").controller("NodeInfoCtrl",["$scope","$modalInstance","_","node",function(a,b,c,d){a.node=d,a.objectKeys=c.without(c.keys(d),"value","$$hashKey"),a.identityFn=c.identity,a.close=function(){b.dismiss("close")}}]),angular.module("etcd.page").controller("EditTtlCtrl",["$scope","$rootScope","$modalInstance","_","ETCD_EVENT","etcdApiSvc","node",function(a,b,c,d,e,f,g){a.node=g,a.save=function(d){g.ttl=d,a.requestPromise=f.save(g).then(function(){b.$broadcast(e.NODE_CHANGED,g),c.close(g)})},a.cancel=function(){c.dismiss("cancel")}}]),angular.module("etcd.page").controller("EditTtlFormCtrl",["$scope",function(a){a.fields={ttl:a.node.ttl||""},a.submit=function(){a.save(a.fields.ttl)}}]),angular.module("etcd.page").controller("StatsCtrl",["$scope","$modal","etcdApiSvc","pollerSvc",function(a,b,c,d){a.followers=null,a.leader=null,a.leaderName=null,a.parseLatencyStats=function(b){a.followers=b.followers,a.leaderName=b.leaderName},a.fetchLeaderDetails=function(){return c.fetchPeerDetailStats(a.leaderName).then(function(b){a.leader={name:a.leaderName,uptime:b.leaderInfo.uptime}})},a.$watch("leaderName",function(b){b&&(d.kill("leaderDetailsPoller"),d.register("leaderDetailsPoller",{fn:a.fetchLeaderDetails,scope:a,interval:5e3}))}),a.openDetailModal=function(a){b.open({templateUrl:"/page/stats/stats-detail.html",controller:"StatsDetailCtrl",resolve:{peerName:d3.functor(a)}})},a.getSquareStatusClass=function(a){return a.latency.current<25?"ed-m-square-status--green":a.latency.current<60?"ed-m-square-status--orange":"ed-m-square-status--red"},d.register("statsPoller",{fn:c.fetchLeaderStats,then:a.parseLatencyStats,scope:a,interval:500})}]),angular.module("etcd.page").controller("StatsDetailCtrl",["$scope","$modalInstance","_","etcdApiSvc","peerName",function(a,b,c,d,e){d.fetchPeerDetailStats(e).then(function(b){a.stats=b,a.objectKeys=c.without(c.keys(a.stats),"$$hashKey")}),a.identityFn=c.identity,a.close=function(){b.dismiss("close")}}]),angular.module("etcd.ui").directive("coBreadcrumb",["_","pathSvc",function(a,b){return{templateUrl:"/ui/breadcrumb.html",restrict:"E",replace:!0,scope:{path:"=",callback:"&"},link:function(c){c.goToRoot=function(){c.callback({path:"/"})},c.onClick=function(d){var e,f;e=c.pathParts.slice(0,c.pathParts.indexOf(d)+1),f=b.make(a.pluck(e,"name")),c.callback({path:f})},c.$watch("path",function(a){c.pathParts=b.explode(a).map(function(a){return{name:a}})})}}}]),angular.module("etcd.ui").directive("edNodeCog",["$modal","$rootScope","etcdApiSvc","toastSvc","ETCD_EVENT",function(a,b,c,d,e){return{templateUrl:"/ui/node-cog.html",restrict:"E",replace:!0,scope:{node:"="},controller:["$scope",function(d){function f(){return d.node.dir?'Are you sure you want to delete the directory "'+d.node.key+'" and all of its keys?':'Are you sure you ant to delete "'+d.node.key+'"'}d.cogDropdownOptions=[{label:"View Details...",callback:function(){a.open({templateUrl:"/page/browser/node-info.html",controller:"NodeInfoCtrl",resolve:{node:d3.functor(d.node)}})},weight:100},{label:"Delete Node...",callback:function(){a.open({templateUrl:"/coreos.ui/confirm-modal/confirm-modal.html",controller:"ConfirmModalCtrl",resolve:{title:d3.functor("Delete Node"),message:f,btnText:d3.functor("Delete"),errorFormatter:d3.functor("etcdApi"),executeFn:_.identity.bind(null,function(){return c["delete"](d.node).then(function(){b.$broadcast(e.NODE_DELETED,d.node)})})}})},weight:300}],d.cogDropdownOptions.push(d.node.dir?{label:"Modify TTL...",callback:function(){a.open({templateUrl:"/page/browser/edit-ttl.html",controller:"EditTtlCtrl",resolve:{node:d3.functor(d.node)}})},weight:200}:{label:"Modify Node...",callback:function(){a.open({templateUrl:"/page/browser/edit-node.html",controller:"EditNodeCtrl",resolve:{node:d3.functor(d.node)}})},weight:200})}]}}]),angular.module("etcd.ui").value("graphConfig",{padding:{top:10,left:5,bottom:40,right:10},data:[{name:"stats"},{name:"thresholds",values:[50,100]}],scales:[{name:"y",type:"ordinal",range:"height",domain:{data:"stats",field:"index"}},{name:"x",range:"width",domainMin:0,domainMax:100,nice:!0,zero:!0,domain:{data:"stats",field:"data.latency.current"}},{name:"color",type:"linear",domain:[10,50,100,1e9],range:["#00DB24","#FFC000","#c40022","#c40022"]}],axes:[{type:"x",scale:"x",ticks:6,name:"Latency (ms)"},{type:"y",scale:"y",properties:{ticks:{stroke:{value:"transparent"}},majorTicks:{stroke:{value:"transparent"}},labels:{fill:{value:"transparent"}},axis:{stroke:{value:"#333"},strokeWidth:{value:1}}}}],marks:[{type:"rect",from:{data:"stats"},properties:{enter:{x:{scale:"x",value:0},x2:{scale:"x",field:"data.latency.current"},y:{scale:"y",field:"index",offset:-1},height:{value:3},fill:{scale:"color",field:"data.latency.current"}}}},{type:"symbol",from:{data:"stats"},properties:{enter:{x:{scale:"x",field:"data.latency.current"},y:{scale:"y",field:"index"},size:{value:50},fill:{value:"#000"}}}}]}),angular.module("etcd.ui").directive("coLatencyGraph",["etcdApiSvc","$","d3","_","graphConfig",function(a,b,c,d,e){return{templateUrl:"/ui/latency-graph.html",restrict:"E",replace:!0,scope:{peerData:"="},link:function(a,b){function c(){var c=b.width()-f,d=300;vg.parse.spec(e,function(e){e({el:b[0],data:{stats:a.peerData}}).width(c).height(d).update()})}var f=60;a.$watch("peerData",function(a){d.isEmpty(a)||c()}),b.on("$destroy",function(){})}}}]); -------------------------------------------------------------------------------- /third_party/github.com/gorilla/mux/mux_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package mux 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "testing" 11 | 12 | "github.com/coreos/etcd/third_party/github.com/gorilla/context" 13 | ) 14 | 15 | type routeTest struct { 16 | title string // title of the test 17 | route *Route // the route being tested 18 | request *http.Request // a request to test the route 19 | vars map[string]string // the expected vars of the match 20 | host string // the expected host of the match 21 | path string // the expected path of the match 22 | shouldMatch bool // whether the request is expected to match the route at all 23 | } 24 | 25 | func TestHost(t *testing.T) { 26 | // newRequestHost a new request with a method, url, and host header 27 | newRequestHost := func(method, url, host string) *http.Request { 28 | req, err := http.NewRequest(method, url, nil) 29 | if err != nil { 30 | panic(err) 31 | } 32 | req.Host = host 33 | return req 34 | } 35 | 36 | tests := []routeTest{ 37 | { 38 | title: "Host route match", 39 | route: new(Route).Host("aaa.bbb.ccc"), 40 | request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 41 | vars: map[string]string{}, 42 | host: "aaa.bbb.ccc", 43 | path: "", 44 | shouldMatch: true, 45 | }, 46 | { 47 | title: "Host route, wrong host in request URL", 48 | route: new(Route).Host("aaa.bbb.ccc"), 49 | request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 50 | vars: map[string]string{}, 51 | host: "aaa.bbb.ccc", 52 | path: "", 53 | shouldMatch: false, 54 | }, 55 | { 56 | title: "Host route with port, match", 57 | route: new(Route).Host("aaa.bbb.ccc:1234"), 58 | request: newRequest("GET", "http://aaa.bbb.ccc:1234/111/222/333"), 59 | vars: map[string]string{}, 60 | host: "aaa.bbb.ccc:1234", 61 | path: "", 62 | shouldMatch: true, 63 | }, 64 | { 65 | title: "Host route with port, wrong port in request URL", 66 | route: new(Route).Host("aaa.bbb.ccc:1234"), 67 | request: newRequest("GET", "http://aaa.bbb.ccc:9999/111/222/333"), 68 | vars: map[string]string{}, 69 | host: "aaa.bbb.ccc:1234", 70 | path: "", 71 | shouldMatch: false, 72 | }, 73 | { 74 | title: "Host route, match with host in request header", 75 | route: new(Route).Host("aaa.bbb.ccc"), 76 | request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc"), 77 | vars: map[string]string{}, 78 | host: "aaa.bbb.ccc", 79 | path: "", 80 | shouldMatch: true, 81 | }, 82 | { 83 | title: "Host route, wrong host in request header", 84 | route: new(Route).Host("aaa.bbb.ccc"), 85 | request: newRequestHost("GET", "/111/222/333", "aaa.222.ccc"), 86 | vars: map[string]string{}, 87 | host: "aaa.bbb.ccc", 88 | path: "", 89 | shouldMatch: false, 90 | }, 91 | // BUG {new(Route).Host("aaa.bbb.ccc:1234"), newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:1234"), map[string]string{}, "aaa.bbb.ccc:1234", "", true}, 92 | { 93 | title: "Host route with port, wrong host in request header", 94 | route: new(Route).Host("aaa.bbb.ccc:1234"), 95 | request: newRequestHost("GET", "/111/222/333", "aaa.bbb.ccc:9999"), 96 | vars: map[string]string{}, 97 | host: "aaa.bbb.ccc:1234", 98 | path: "", 99 | shouldMatch: false, 100 | }, 101 | { 102 | title: "Host route with pattern, match", 103 | route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), 104 | request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 105 | vars: map[string]string{"v1": "bbb"}, 106 | host: "aaa.bbb.ccc", 107 | path: "", 108 | shouldMatch: true, 109 | }, 110 | { 111 | title: "Host route with pattern, wrong host in request URL", 112 | route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc"), 113 | request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 114 | vars: map[string]string{"v1": "bbb"}, 115 | host: "aaa.bbb.ccc", 116 | path: "", 117 | shouldMatch: false, 118 | }, 119 | { 120 | title: "Host route with multiple patterns, match", 121 | route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), 122 | request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 123 | vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, 124 | host: "aaa.bbb.ccc", 125 | path: "", 126 | shouldMatch: true, 127 | }, 128 | { 129 | title: "Host route with multiple patterns, wrong host in request URL", 130 | route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}"), 131 | request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 132 | vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc"}, 133 | host: "aaa.bbb.ccc", 134 | path: "", 135 | shouldMatch: false, 136 | }, 137 | } 138 | for _, test := range tests { 139 | testRoute(t, test) 140 | } 141 | } 142 | 143 | func TestPath(t *testing.T) { 144 | tests := []routeTest{ 145 | { 146 | title: "Path route, match", 147 | route: new(Route).Path("/111/222/333"), 148 | request: newRequest("GET", "http://localhost/111/222/333"), 149 | vars: map[string]string{}, 150 | host: "", 151 | path: "/111/222/333", 152 | shouldMatch: true, 153 | }, 154 | { 155 | title: "Path route, wrong path in request in request URL", 156 | route: new(Route).Path("/111/222/333"), 157 | request: newRequest("GET", "http://localhost/1/2/3"), 158 | vars: map[string]string{}, 159 | host: "", 160 | path: "/111/222/333", 161 | shouldMatch: false, 162 | }, 163 | { 164 | title: "Path route with pattern, match", 165 | route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), 166 | request: newRequest("GET", "http://localhost/111/222/333"), 167 | vars: map[string]string{"v1": "222"}, 168 | host: "", 169 | path: "/111/222/333", 170 | shouldMatch: true, 171 | }, 172 | { 173 | title: "Path route with pattern, URL in request does not match", 174 | route: new(Route).Path("/111/{v1:[0-9]{3}}/333"), 175 | request: newRequest("GET", "http://localhost/111/aaa/333"), 176 | vars: map[string]string{"v1": "222"}, 177 | host: "", 178 | path: "/111/222/333", 179 | shouldMatch: false, 180 | }, 181 | { 182 | title: "Path route with multiple patterns, match", 183 | route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), 184 | request: newRequest("GET", "http://localhost/111/222/333"), 185 | vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, 186 | host: "", 187 | path: "/111/222/333", 188 | shouldMatch: true, 189 | }, 190 | { 191 | title: "Path route with multiple patterns, URL in request does not match", 192 | route: new(Route).Path("/{v1:[0-9]{3}}/{v2:[0-9]{3}}/{v3:[0-9]{3}}"), 193 | request: newRequest("GET", "http://localhost/111/aaa/333"), 194 | vars: map[string]string{"v1": "111", "v2": "222", "v3": "333"}, 195 | host: "", 196 | path: "/111/222/333", 197 | shouldMatch: false, 198 | }, 199 | } 200 | 201 | for _, test := range tests { 202 | testRoute(t, test) 203 | } 204 | } 205 | 206 | func TestPathPrefix(t *testing.T) { 207 | tests := []routeTest{ 208 | { 209 | title: "PathPrefix route, match", 210 | route: new(Route).PathPrefix("/111"), 211 | request: newRequest("GET", "http://localhost/111/222/333"), 212 | vars: map[string]string{}, 213 | host: "", 214 | path: "/111", 215 | shouldMatch: true, 216 | }, 217 | { 218 | title: "PathPrefix route, URL prefix in request does not match", 219 | route: new(Route).PathPrefix("/111"), 220 | request: newRequest("GET", "http://localhost/1/2/3"), 221 | vars: map[string]string{}, 222 | host: "", 223 | path: "/111", 224 | shouldMatch: false, 225 | }, 226 | { 227 | title: "PathPrefix route with pattern, match", 228 | route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), 229 | request: newRequest("GET", "http://localhost/111/222/333"), 230 | vars: map[string]string{"v1": "222"}, 231 | host: "", 232 | path: "/111/222", 233 | shouldMatch: true, 234 | }, 235 | { 236 | title: "PathPrefix route with pattern, URL prefix in request does not match", 237 | route: new(Route).PathPrefix("/111/{v1:[0-9]{3}}"), 238 | request: newRequest("GET", "http://localhost/111/aaa/333"), 239 | vars: map[string]string{"v1": "222"}, 240 | host: "", 241 | path: "/111/222", 242 | shouldMatch: false, 243 | }, 244 | { 245 | title: "PathPrefix route with multiple patterns, match", 246 | route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), 247 | request: newRequest("GET", "http://localhost/111/222/333"), 248 | vars: map[string]string{"v1": "111", "v2": "222"}, 249 | host: "", 250 | path: "/111/222", 251 | shouldMatch: true, 252 | }, 253 | { 254 | title: "PathPrefix route with multiple patterns, URL prefix in request does not match", 255 | route: new(Route).PathPrefix("/{v1:[0-9]{3}}/{v2:[0-9]{3}}"), 256 | request: newRequest("GET", "http://localhost/111/aaa/333"), 257 | vars: map[string]string{"v1": "111", "v2": "222"}, 258 | host: "", 259 | path: "/111/222", 260 | shouldMatch: false, 261 | }, 262 | } 263 | 264 | for _, test := range tests { 265 | testRoute(t, test) 266 | } 267 | } 268 | 269 | func TestHostPath(t *testing.T) { 270 | tests := []routeTest{ 271 | { 272 | title: "Host and Path route, match", 273 | route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), 274 | request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 275 | vars: map[string]string{}, 276 | host: "", 277 | path: "", 278 | shouldMatch: true, 279 | }, 280 | { 281 | title: "Host and Path route, wrong host in request URL", 282 | route: new(Route).Host("aaa.bbb.ccc").Path("/111/222/333"), 283 | request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 284 | vars: map[string]string{}, 285 | host: "", 286 | path: "", 287 | shouldMatch: false, 288 | }, 289 | { 290 | title: "Host and Path route with pattern, match", 291 | route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), 292 | request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 293 | vars: map[string]string{"v1": "bbb", "v2": "222"}, 294 | host: "aaa.bbb.ccc", 295 | path: "/111/222/333", 296 | shouldMatch: true, 297 | }, 298 | { 299 | title: "Host and Path route with pattern, URL in request does not match", 300 | route: new(Route).Host("aaa.{v1:[a-z]{3}}.ccc").Path("/111/{v2:[0-9]{3}}/333"), 301 | request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 302 | vars: map[string]string{"v1": "bbb", "v2": "222"}, 303 | host: "aaa.bbb.ccc", 304 | path: "/111/222/333", 305 | shouldMatch: false, 306 | }, 307 | { 308 | title: "Host and Path route with multiple patterns, match", 309 | route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), 310 | request: newRequest("GET", "http://aaa.bbb.ccc/111/222/333"), 311 | vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, 312 | host: "aaa.bbb.ccc", 313 | path: "/111/222/333", 314 | shouldMatch: true, 315 | }, 316 | { 317 | title: "Host and Path route with multiple patterns, URL in request does not match", 318 | route: new(Route).Host("{v1:[a-z]{3}}.{v2:[a-z]{3}}.{v3:[a-z]{3}}").Path("/{v4:[0-9]{3}}/{v5:[0-9]{3}}/{v6:[0-9]{3}}"), 319 | request: newRequest("GET", "http://aaa.222.ccc/111/222/333"), 320 | vars: map[string]string{"v1": "aaa", "v2": "bbb", "v3": "ccc", "v4": "111", "v5": "222", "v6": "333"}, 321 | host: "aaa.bbb.ccc", 322 | path: "/111/222/333", 323 | shouldMatch: false, 324 | }, 325 | } 326 | 327 | for _, test := range tests { 328 | testRoute(t, test) 329 | } 330 | } 331 | 332 | func TestHeaders(t *testing.T) { 333 | // newRequestHeaders creates a new request with a method, url, and headers 334 | newRequestHeaders := func(method, url string, headers map[string]string) *http.Request { 335 | req, err := http.NewRequest(method, url, nil) 336 | if err != nil { 337 | panic(err) 338 | } 339 | for k, v := range headers { 340 | req.Header.Add(k, v) 341 | } 342 | return req 343 | } 344 | 345 | tests := []routeTest{ 346 | { 347 | title: "Headers route, match", 348 | route: new(Route).Headers("foo", "bar", "baz", "ding"), 349 | request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "ding"}), 350 | vars: map[string]string{}, 351 | host: "", 352 | path: "", 353 | shouldMatch: true, 354 | }, 355 | { 356 | title: "Headers route, bad header values", 357 | route: new(Route).Headers("foo", "bar", "baz", "ding"), 358 | request: newRequestHeaders("GET", "http://localhost", map[string]string{"foo": "bar", "baz": "dong"}), 359 | vars: map[string]string{}, 360 | host: "", 361 | path: "", 362 | shouldMatch: false, 363 | }, 364 | } 365 | 366 | for _, test := range tests { 367 | testRoute(t, test) 368 | } 369 | 370 | } 371 | 372 | func TestMethods(t *testing.T) { 373 | tests := []routeTest{ 374 | { 375 | title: "Methods route, match GET", 376 | route: new(Route).Methods("GET", "POST"), 377 | request: newRequest("GET", "http://localhost"), 378 | vars: map[string]string{}, 379 | host: "", 380 | path: "", 381 | shouldMatch: true, 382 | }, 383 | { 384 | title: "Methods route, match POST", 385 | route: new(Route).Methods("GET", "POST"), 386 | request: newRequest("POST", "http://localhost"), 387 | vars: map[string]string{}, 388 | host: "", 389 | path: "", 390 | shouldMatch: true, 391 | }, 392 | { 393 | title: "Methods route, bad method", 394 | route: new(Route).Methods("GET", "POST"), 395 | request: newRequest("PUT", "http://localhost"), 396 | vars: map[string]string{}, 397 | host: "", 398 | path: "", 399 | shouldMatch: false, 400 | }, 401 | } 402 | 403 | for _, test := range tests { 404 | testRoute(t, test) 405 | } 406 | } 407 | 408 | func TestQueries(t *testing.T) { 409 | tests := []routeTest{ 410 | { 411 | title: "Queries route, match", 412 | route: new(Route).Queries("foo", "bar", "baz", "ding"), 413 | request: newRequest("GET", "http://localhost?foo=bar&baz=ding"), 414 | vars: map[string]string{}, 415 | host: "", 416 | path: "", 417 | shouldMatch: true, 418 | }, 419 | { 420 | title: "Queries route, match with a query string", 421 | route: new(Route).Host("www.example.com").Path("/api").Queries("foo", "bar", "baz", "ding"), 422 | request: newRequest("GET", "http://www.example.com/api?foo=bar&baz=ding"), 423 | vars: map[string]string{}, 424 | host: "", 425 | path: "", 426 | shouldMatch: true, 427 | }, 428 | { 429 | title: "Queries route, bad query", 430 | route: new(Route).Queries("foo", "bar", "baz", "ding"), 431 | request: newRequest("GET", "http://localhost?foo=bar&baz=dong"), 432 | vars: map[string]string{}, 433 | host: "", 434 | path: "", 435 | shouldMatch: false, 436 | }, 437 | } 438 | 439 | for _, test := range tests { 440 | testRoute(t, test) 441 | } 442 | } 443 | 444 | func TestSchemes(t *testing.T) { 445 | tests := []routeTest{ 446 | // Schemes 447 | { 448 | title: "Schemes route, match https", 449 | route: new(Route).Schemes("https", "ftp"), 450 | request: newRequest("GET", "https://localhost"), 451 | vars: map[string]string{}, 452 | host: "", 453 | path: "", 454 | shouldMatch: true, 455 | }, 456 | { 457 | title: "Schemes route, match ftp", 458 | route: new(Route).Schemes("https", "ftp"), 459 | request: newRequest("GET", "ftp://localhost"), 460 | vars: map[string]string{}, 461 | host: "", 462 | path: "", 463 | shouldMatch: true, 464 | }, 465 | { 466 | title: "Schemes route, bad scheme", 467 | route: new(Route).Schemes("https", "ftp"), 468 | request: newRequest("GET", "http://localhost"), 469 | vars: map[string]string{}, 470 | host: "", 471 | path: "", 472 | shouldMatch: false, 473 | }, 474 | } 475 | for _, test := range tests { 476 | testRoute(t, test) 477 | } 478 | } 479 | 480 | func TestMatcherFunc(t *testing.T) { 481 | m := func(r *http.Request, m *RouteMatch) bool { 482 | if r.URL.Host == "aaa.bbb.ccc" { 483 | return true 484 | } 485 | return false 486 | } 487 | 488 | tests := []routeTest{ 489 | { 490 | title: "MatchFunc route, match", 491 | route: new(Route).MatcherFunc(m), 492 | request: newRequest("GET", "http://aaa.bbb.ccc"), 493 | vars: map[string]string{}, 494 | host: "", 495 | path: "", 496 | shouldMatch: true, 497 | }, 498 | { 499 | title: "MatchFunc route, non-match", 500 | route: new(Route).MatcherFunc(m), 501 | request: newRequest("GET", "http://aaa.222.ccc"), 502 | vars: map[string]string{}, 503 | host: "", 504 | path: "", 505 | shouldMatch: false, 506 | }, 507 | } 508 | 509 | for _, test := range tests { 510 | testRoute(t, test) 511 | } 512 | } 513 | 514 | func TestSubRouter(t *testing.T) { 515 | subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter() 516 | subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter() 517 | 518 | tests := []routeTest{ 519 | { 520 | route: subrouter1.Path("/{v2:[a-z]+}"), 521 | request: newRequest("GET", "http://aaa.google.com/bbb"), 522 | vars: map[string]string{"v1": "aaa", "v2": "bbb"}, 523 | host: "aaa.google.com", 524 | path: "/bbb", 525 | shouldMatch: true, 526 | }, 527 | { 528 | route: subrouter1.Path("/{v2:[a-z]+}"), 529 | request: newRequest("GET", "http://111.google.com/111"), 530 | vars: map[string]string{"v1": "aaa", "v2": "bbb"}, 531 | host: "aaa.google.com", 532 | path: "/bbb", 533 | shouldMatch: false, 534 | }, 535 | { 536 | route: subrouter2.Path("/baz/{v2}"), 537 | request: newRequest("GET", "http://localhost/foo/bar/baz/ding"), 538 | vars: map[string]string{"v1": "bar", "v2": "ding"}, 539 | host: "", 540 | path: "/foo/bar/baz/ding", 541 | shouldMatch: true, 542 | }, 543 | { 544 | route: subrouter2.Path("/baz/{v2}"), 545 | request: newRequest("GET", "http://localhost/foo/bar"), 546 | vars: map[string]string{"v1": "bar", "v2": "ding"}, 547 | host: "", 548 | path: "/foo/bar/baz/ding", 549 | shouldMatch: false, 550 | }, 551 | } 552 | 553 | for _, test := range tests { 554 | testRoute(t, test) 555 | } 556 | } 557 | 558 | func TestNamedRoutes(t *testing.T) { 559 | r1 := NewRouter() 560 | r1.NewRoute().Name("a") 561 | r1.NewRoute().Name("b") 562 | r1.NewRoute().Name("c") 563 | 564 | r2 := r1.NewRoute().Subrouter() 565 | r2.NewRoute().Name("d") 566 | r2.NewRoute().Name("e") 567 | r2.NewRoute().Name("f") 568 | 569 | r3 := r2.NewRoute().Subrouter() 570 | r3.NewRoute().Name("g") 571 | r3.NewRoute().Name("h") 572 | r3.NewRoute().Name("i") 573 | 574 | if r1.namedRoutes == nil || len(r1.namedRoutes) != 9 { 575 | t.Errorf("Expected 9 named routes, got %v", r1.namedRoutes) 576 | } else if r1.Get("i") == nil { 577 | t.Errorf("Subroute name not registered") 578 | } 579 | } 580 | 581 | func TestStrictSlash(t *testing.T) { 582 | var r *Router 583 | var req *http.Request 584 | var route *Route 585 | var match *RouteMatch 586 | var matched bool 587 | 588 | // StrictSlash should be ignored for path prefix. 589 | // So we register a route ending in slash but it doesn't attempt to add 590 | // the slash for a path not ending in slash. 591 | r = NewRouter() 592 | r.StrictSlash(true) 593 | route = r.NewRoute().PathPrefix("/static/") 594 | req, _ = http.NewRequest("GET", "http://localhost/static/logo.png", nil) 595 | match = new(RouteMatch) 596 | matched = r.Match(req, match) 597 | if !matched { 598 | t.Errorf("Should match request %q -- %v", req.URL.Path, getRouteTemplate(route)) 599 | } 600 | if match.Handler != nil { 601 | t.Errorf("Should not redirect") 602 | } 603 | } 604 | 605 | // ---------------------------------------------------------------------------- 606 | // Helpers 607 | // ---------------------------------------------------------------------------- 608 | 609 | func getRouteTemplate(route *Route) string { 610 | host, path := "none", "none" 611 | if route.regexp != nil { 612 | if route.regexp.host != nil { 613 | host = route.regexp.host.template 614 | } 615 | if route.regexp.path != nil { 616 | path = route.regexp.path.template 617 | } 618 | } 619 | return fmt.Sprintf("Host: %v, Path: %v", host, path) 620 | } 621 | 622 | func testRoute(t *testing.T, test routeTest) { 623 | request := test.request 624 | route := test.route 625 | vars := test.vars 626 | shouldMatch := test.shouldMatch 627 | host := test.host 628 | path := test.path 629 | url := test.host + test.path 630 | 631 | var match RouteMatch 632 | ok := route.Match(request, &match) 633 | if ok != shouldMatch { 634 | msg := "Should match" 635 | if !shouldMatch { 636 | msg = "Should not match" 637 | } 638 | t.Errorf("(%v) %v:\nRoute: %#v\nRequest: %#v\nVars: %v\n", test.title, msg, route, request, vars) 639 | return 640 | } 641 | if shouldMatch { 642 | if test.vars != nil && !stringMapEqual(test.vars, match.Vars) { 643 | t.Errorf("(%v) Vars not equal: expected %v, got %v", test.title, vars, match.Vars) 644 | return 645 | } 646 | if host != "" { 647 | u, _ := test.route.URLHost(mapToPairs(match.Vars)...) 648 | if host != u.Host { 649 | t.Errorf("(%v) URLHost not equal: expected %v, got %v -- %v", test.title, host, u.Host, getRouteTemplate(route)) 650 | return 651 | } 652 | } 653 | if path != "" { 654 | u, _ := route.URLPath(mapToPairs(match.Vars)...) 655 | if path != u.Path { 656 | t.Errorf("(%v) URLPath not equal: expected %v, got %v -- %v", test.title, path, u.Path, getRouteTemplate(route)) 657 | return 658 | } 659 | } 660 | if url != "" { 661 | u, _ := route.URL(mapToPairs(match.Vars)...) 662 | if url != u.Host+u.Path { 663 | t.Errorf("(%v) URL not equal: expected %v, got %v -- %v", test.title, url, u.Host+u.Path, getRouteTemplate(route)) 664 | return 665 | } 666 | } 667 | } 668 | } 669 | 670 | // Tests that the context is cleared or not cleared properly depending on 671 | // the configuration of the router 672 | func TestKeepContext(t *testing.T) { 673 | func1 := func(w http.ResponseWriter, r *http.Request) {} 674 | 675 | r := NewRouter() 676 | r.HandleFunc("/", func1).Name("func1") 677 | 678 | req, _ := http.NewRequest("GET", "http://localhost/", nil) 679 | context.Set(req, "t", 1) 680 | 681 | res := new(http.ResponseWriter) 682 | r.ServeHTTP(*res, req) 683 | 684 | if _, ok := context.GetOk(req, "t"); ok { 685 | t.Error("Context should have been cleared at end of request") 686 | } 687 | 688 | r.KeepContext = true 689 | 690 | req, _ = http.NewRequest("GET", "http://localhost/", nil) 691 | context.Set(req, "t", 1) 692 | 693 | r.ServeHTTP(*res, req) 694 | if _, ok := context.GetOk(req, "t"); !ok { 695 | t.Error("Context should NOT have been cleared at end of request") 696 | } 697 | 698 | } 699 | 700 | type TestA301ResponseWriter struct { 701 | hh http.Header 702 | status int 703 | } 704 | 705 | func (ho TestA301ResponseWriter) Header() http.Header { 706 | return http.Header(ho.hh) 707 | } 708 | 709 | func (ho TestA301ResponseWriter) Write(b []byte) (int, error) { 710 | return 0, nil 711 | } 712 | 713 | func (ho TestA301ResponseWriter) WriteHeader(code int) { 714 | ho.status = code 715 | } 716 | 717 | func Test301Redirect(t *testing.T) { 718 | m := make(http.Header) 719 | 720 | func1 := func(w http.ResponseWriter, r *http.Request) {} 721 | func2 := func(w http.ResponseWriter, r *http.Request) {} 722 | 723 | r := NewRouter() 724 | r.HandleFunc("/api/", func2).Name("func2") 725 | r.HandleFunc("/", func1).Name("func1") 726 | 727 | req, _ := http.NewRequest("GET", "http://localhost//api/?abc=def", nil) 728 | 729 | res := TestA301ResponseWriter{ 730 | hh: m, 731 | status: 0, 732 | } 733 | r.ServeHTTP(&res, req) 734 | 735 | if "http://localhost/api/?abc=def" != res.hh["Location"][0] { 736 | t.Errorf("Should have complete URL with query string") 737 | } 738 | } 739 | 740 | // https://plus.google.com/101022900381697718949/posts/eWy6DjFJ6uW 741 | func TestSubrouterHeader(t *testing.T) { 742 | expected := "func1 response" 743 | func1 := func(w http.ResponseWriter, r *http.Request) { 744 | fmt.Fprint(w, expected) 745 | } 746 | func2 := func(http.ResponseWriter, *http.Request) {} 747 | 748 | r := NewRouter() 749 | s := r.Headers("SomeSpecialHeader", "").Subrouter() 750 | s.HandleFunc("/", func1).Name("func1") 751 | r.HandleFunc("/", func2).Name("func2") 752 | 753 | req, _ := http.NewRequest("GET", "http://localhost/", nil) 754 | req.Header.Add("SomeSpecialHeader", "foo") 755 | match := new(RouteMatch) 756 | matched := r.Match(req, match) 757 | if !matched { 758 | t.Errorf("Should match request") 759 | } 760 | if match.Route.GetName() != "func1" { 761 | t.Errorf("Expecting func1 handler, got %s", match.Route.GetName()) 762 | } 763 | resp := NewRecorder() 764 | match.Handler.ServeHTTP(resp, req) 765 | if resp.Body.String() != expected { 766 | t.Errorf("Expecting %q", expected) 767 | } 768 | } 769 | 770 | // mapToPairs converts a string map to a slice of string pairs 771 | func mapToPairs(m map[string]string) []string { 772 | var i int 773 | p := make([]string, len(m)*2) 774 | for k, v := range m { 775 | p[i] = k 776 | p[i+1] = v 777 | i += 2 778 | } 779 | return p 780 | } 781 | 782 | // stringMapEqual checks the equality of two string maps 783 | func stringMapEqual(m1, m2 map[string]string) bool { 784 | nil1 := m1 == nil 785 | nil2 := m2 == nil 786 | if nil1 != nil2 || len(m1) != len(m2) { 787 | return false 788 | } 789 | for k, v := range m1 { 790 | if v != m2[k] { 791 | return false 792 | } 793 | } 794 | return true 795 | } 796 | 797 | // newRequest is a helper function to create a new request with a method and url 798 | func newRequest(method, url string) *http.Request { 799 | req, err := http.NewRequest(method, url, nil) 800 | if err != nil { 801 | panic(err) 802 | } 803 | return req 804 | } 805 | -------------------------------------------------------------------------------- /dashboard/dist/coreos-web/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 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 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | --------------------------------------------------------------------------------