diff --git a/Makefile b/Makefile index ef888f5bf..6ec99023a 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,7 @@ container: all .PHONY: test-unit test-unit: @echo Running tests... - go test -v $(GO_PKGS) + go test -race -v $(GO_PKGS) .PHONY: test-flake8 test-flake8: diff --git a/cmd/manager/deployments.go b/cmd/manager/deployments.go index 993dc1678..e71a2418e 100644 --- a/cmd/manager/deployments.go +++ b/cmd/manager/deployments.go @@ -18,7 +18,7 @@ package main import ( "encoding/json" - "flag" + "errors" "fmt" "io" "io/ioutil" @@ -32,11 +32,11 @@ import ( "github.com/ghodss/yaml" "github.com/gorilla/mux" - "github.com/kubernetes/deployment-manager/cmd/manager/manager" "github.com/kubernetes/deployment-manager/cmd/manager/repository" "github.com/kubernetes/deployment-manager/cmd/manager/repository/persistent" "github.com/kubernetes/deployment-manager/cmd/manager/repository/transient" + "github.com/kubernetes/deployment-manager/cmd/manager/router" "github.com/kubernetes/deployment-manager/pkg/common" "github.com/kubernetes/deployment-manager/pkg/registry" "github.com/kubernetes/deployment-manager/pkg/util" @@ -65,57 +65,84 @@ var deployments = []Route{ {"GetCredential", "/credentials/{credential}", "GET", getCredentialHandlerFunc, ""}, } -var ( - maxLength = flag.Int64("maxLength", 1024, "The maximum length (KB) of a template.") - expanderName = flag.String("expander", "expandybird-service", "The DNS name of the expander service.") - expanderURL = flag.String("expanderURL", "", "The URL for the expander service.") - deployerName = flag.String("deployer", "resourcifier-service", "The DNS name of the deployer service.") - deployerURL = flag.String("deployerURL", "", "The URL for the deployer service.") - credentialFile = flag.String("credentialFile", "", "Local file to use for credentials.") - credentialSecrets = flag.Bool("credentialSecrets", true, "Use secrets for credentials.") - mongoName = flag.String("mongoName", "mongodb", "The DNS name of the mongodb service.") - mongoPort = flag.String("mongoPort", "27017", "The port of the mongodb service.") - mongoAddress = flag.String("mongoAddress", "mongodb:27017", "The address of the mongodb service.") -) - +// Deprecated. Use Context.Manager instead. var backend manager.Manager -func init() { - if !flag.Parsed() { - flag.Parse() +// Route defines a routing table entry to be registered with gorilla/mux. +// +// Route is deprecated. Use router.Routes instead. +type Route struct { + Name string + Path string + Methods string + HandlerFunc http.HandlerFunc + Type string +} + +func registerRoutes(c *router.Context) router.Routes { + re := regexp.MustCompile("{[a-z]+}") + + r := router.NewRoutes() + r.Add("GET /healthz", healthz) + + // TODO: Replace these routes with updated ones. + for _, d := range deployments { + path := fmt.Sprintf("%s %s", d.Methods, re.ReplaceAllString(d.Path, "*")) + fmt.Printf("\t%s\n", path) + r.Add(path, func(w http.ResponseWriter, r *http.Request, c *router.Context) error { + d.HandlerFunc(w, r) + return nil + }) } - routes = append(routes, deployments...) + return r +} + +func healthz(w http.ResponseWriter, r *http.Request, c *router.Context) error { + c.Log("manager: healthz checkpoint") + // TODO: This should check the availability of the repository, and fail if it + // cannot connect. + fmt.Fprintln(w, "OK") + return nil +} + +func setupDependencies(c *router.Context) error { var credentialProvider common.CredentialProvider - if *credentialFile != "" { - if *credentialSecrets { - panic(fmt.Errorf("Both credentialFile and credentialSecrets are set")) + if c.Config.CredentialFile != "" { + if c.Config.CredentialSecrets { + return errors.New("Both credentialFile and credentialSecrets are set") } var err error - credentialProvider, err = registry.NewFilebasedCredentialProvider(*credentialFile) + credentialProvider, err = registry.NewFilebasedCredentialProvider(c.Config.CredentialFile) if err != nil { - panic(fmt.Errorf("cannot create credential provider %s: %s", *credentialFile, err)) + return fmt.Errorf("cannot create credential provider %s: %s", c.Config.CredentialFile, err) } } else if *credentialSecrets { credentialProvider = registry.NewSecretsCredentialProvider() } else { credentialProvider = registry.NewInmemCredentialProvider() } - backend = newManager(credentialProvider) + c.CredentialProvider = credentialProvider + c.Manager = newManager(c) + + // FIXME: As soon as we can, we need to get rid of this. + backend = c.Manager + return nil } const expanderPort = "8080" const deployerPort = "8080" -func newManager(cp common.CredentialProvider) manager.Manager { +func newManager(c *router.Context) manager.Manager { + cfg := c.Config service := registry.NewInmemRegistryService() - registryProvider := registry.NewDefaultRegistryProvider(cp, service) + registryProvider := registry.NewDefaultRegistryProvider(c.CredentialProvider, service) resolver := manager.NewTypeResolver(registryProvider, util.DefaultHTTPClient()) - expander := manager.NewExpander(getServiceURL(*expanderURL, *expanderName, expanderPort), resolver) - deployer := manager.NewDeployer(getServiceURL(*deployerURL, *deployerName, deployerPort)) - address := strings.TrimPrefix(getServiceURL(*mongoAddress, *mongoName, *mongoPort), "http://") + expander := manager.NewExpander(getServiceURL(cfg.ExpanderURL, cfg.ExpanderName, expanderPort), resolver) + deployer := manager.NewDeployer(getServiceURL(cfg.DeployerURL, cfg.DeployerName, deployerPort)) + address := strings.TrimPrefix(getServiceURL(cfg.MongoAddress, cfg.MongoName, cfg.MongoPort), "http://") repository := createRepository(address) - return manager.NewManager(expander, deployer, repository, registryProvider, service, cp) + return manager.NewManager(expander, deployer, repository, registryProvider, service, c.CredentialProvider) } func createRepository(address string) repository.Repository { diff --git a/cmd/manager/main.go b/cmd/manager/main.go index a4c5df85a..7aca7e80a 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -17,75 +17,64 @@ limitations under the License. package main import ( - "github.com/kubernetes/deployment-manager/pkg/util" + "github.com/kubernetes/deployment-manager/cmd/manager/router" "github.com/kubernetes/deployment-manager/pkg/version" "flag" "fmt" - "log" "net/http" "os" - - "github.com/gorilla/handlers" - "github.com/gorilla/mux" ) -// Route defines a routing table entry to be registered with gorilla/mux. -type Route struct { - Name string - Path string - Methods string - HandlerFunc http.HandlerFunc - Type string -} - -var routes = []Route{ - {"HealthCheck", "/healthz", "GET", healthCheckHandlerFunc, ""}, -} - -// port to listen on -var port = flag.Int("port", 8080, "The port to listen on") +var ( + port = flag.Int("port", 8080, "The port to listen on") + maxLength = flag.Int64("maxLength", 1024, "The maximum length (KB) of a template.") + expanderName = flag.String("expander", "expandybird-service", "The DNS name of the expander service.") + expanderURL = flag.String("expanderURL", "", "The URL for the expander service.") + deployerName = flag.String("deployer", "resourcifier-service", "The DNS name of the deployer service.") + deployerURL = flag.String("deployerURL", "", "The URL for the deployer service.") + credentialFile = flag.String("credentialFile", "", "Local file to use for credentials.") + credentialSecrets = flag.Bool("credentialSecrets", true, "Use secrets for credentials.") + mongoName = flag.String("mongoName", "mongodb", "The DNS name of the mongodb service.") + mongoPort = flag.String("mongoPort", "27017", "The port of the mongodb service.") + mongoAddress = flag.String("mongoAddress", "mongodb:27017", "The address of the mongodb service.") +) -func init() { - if !flag.Parsed() { - flag.Parse() +func main() { + // Set up dependencies + c := &router.Context{ + Config: parseFlags(), } -} -func main() { - if !flag.Parsed() { - flag.Parse() + if err := setupDependencies(c); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) } - router := mux.NewRouter() - router.StrictSlash(true) - for _, route := range routes { - handler := http.Handler(http.HandlerFunc(route.HandlerFunc)) - switch route.Type { - case "JSON": - handler = handlers.ContentTypeHandler(handler, "application/json") - case "": - break - default: - log.Fatalf("invalid route type: %v", route.Type) - } + // Set up routes + routes := registerRoutes(c) - r := router.NewRoute() - r.Name(route.Name). - Path(route.Path). - Methods(route.Methods). - Handler(handler) + // Now create a server. + c.Log("Starting Manager %s on %s", version.Version, c.Config.Address) + if err := http.ListenAndServe(c.Config.Address, router.NewHandler(c, routes)); err != nil { + c.Err("Server exited with error %s", err) + os.Exit(1) } - - address := fmt.Sprintf(":%d", *port) - handler := handlers.CombinedLoggingHandler(os.Stderr, router) - log.Printf("Version: %s", version.Version) - log.Printf("Listening on port %d...", *port) - log.Fatal(http.ListenAndServe(address, handler)) } -func healthCheckHandlerFunc(w http.ResponseWriter, r *http.Request) { - handler := "manager: get health" - util.LogHandlerEntry(handler, r) - util.LogHandlerExitWithText(handler, w, "OK", http.StatusOK) +func parseFlags() *router.Config { + flag.Parse() + return &router.Config{ + Address: fmt.Sprintf(":%d", *port), + MaxTemplateLength: *maxLength, + ExpanderName: *expanderName, + ExpanderURL: *expanderURL, + DeployerName: *deployerName, + DeployerURL: *deployerURL, + CredentialFile: *credentialFile, + CredentialSecrets: *credentialSecrets, + MongoName: *mongoName, + MongoPort: *mongoPort, + MongoAddress: *mongoAddress, + } } diff --git a/cmd/manager/router/encoder.go b/cmd/manager/router/encoder.go new file mode 100644 index 000000000..fd401cb59 --- /dev/null +++ b/cmd/manager/router/encoder.go @@ -0,0 +1,121 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package router + +import ( + "encoding/json" + "errors" + "fmt" + "mime" + "net/http" + "reflect" + "strings" + + "github.com/ghodss/yaml" +) + +type Encoder interface { + // Encode encoders a given response + // + // When an encoder fails, it logs any necessary data and then responds to + // the client. + Encode(http.ResponseWriter, *http.Request, *Context, interface{}) +} + +// AcceptEncodder uses the accept headers on a request to determine the response type. +// +// It supports the following encodings: +// - application/json: passed to encoding/json.Marshal +// - text/yaml: passed to gopkg.in/yaml.v2.Marshal +// - text/plain: passed to fmt.Sprintf("%V") +type AcceptEncoder struct { + DefaultEncoding string +} + +// Encode encodeds the given interface to the first available type in the Accept header. +func (e *AcceptEncoder) Encode(w http.ResponseWriter, r *http.Request, c *Context, out interface{}) { + a := r.Header.Get("accept") + fn := encoders[e.DefaultEncoding] + mt := e.DefaultEncoding + if a != "" { + mt, fn = e.parseAccept(a) + } + + data, err := fn(out) + if err != nil { + Fatal(w, r, "Could not marshal data: %s", err) + return + } + w.Header().Add("content-type", mt) + w.Write(data) +} + +// parseAccept parses the value of an Accept: header and returns the best match. +// +// This returns the matched MIME type and the Marshal function. +func (e *AcceptEncoder) parseAccept(h string) (string, Marshaler) { + + keys := strings.Split(h, ",") + for _, k := range keys { + mt, _, err := mime.ParseMediaType(k) + if err != nil { + continue + } + if enc, ok := encoders[mt]; ok { + return mt, enc + } + } + return e.DefaultEncoding, encoders[e.DefaultEncoding] +} + +type Marshaler func(interface{}) ([]byte, error) + +var encoders = map[string]Marshaler{ + "application/json": json.Marshal, + "text/yaml": yaml.Marshal, + "application/x-yaml": yaml.Marshal, + "text/plain": textMarshal, +} + +var ErrUnsupportedKind = errors.New("unsupported kind") + +// textMarshal marshals v into a text representation ONLY IN NARROW CASES. +// +// An error will have its Error() method called. +// A fmt.Stringer will have its String() method called. +// Scalar types will be marshaled with fmt.Sprintf("%v"). +// +// This will only marshal scalar types for securoty reasons (namely, we don't +// want the possibility of forcing exposure of non-exported data or ptr +// addresses, etc.) +func textMarshal(v interface{}) ([]byte, error) { + switch s := v.(type) { + case error: + return []byte(s.Error()), nil + case fmt.Stringer: + return []byte(s.String()), nil + } + + // Error on kinds we don't support. + val := reflect.Indirect(reflect.ValueOf(v)) + switch val.Kind() { + case reflect.Invalid, reflect.Array, reflect.Chan, reflect.Func, reflect.Interface, + reflect.Map, reflect.Ptr, reflect.Slice, reflect.Struct, reflect.UnsafePointer: + return []byte{}, ErrUnsupportedKind + } + return []byte(fmt.Sprintf("%v", v)), nil +} diff --git a/cmd/manager/router/encoder_test.go b/cmd/manager/router/encoder_test.go new file mode 100644 index 000000000..283581b3e --- /dev/null +++ b/cmd/manager/router/encoder_test.go @@ -0,0 +1,110 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package router + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "testing" +) + +var _ Encoder = &AcceptEncoder{} + +func TestParseAccept(t *testing.T) { + e := &AcceptEncoder{ + DefaultEncoding: "application/json", + } + tests := map[string]string{ + "": e.DefaultEncoding, + "*/*": e.DefaultEncoding, + // To stay true to spec, this _should_ be an error. But our thought + // on this case is that we'd rather send a default format. + "audio/*; q=0.2, audio/basic": e.DefaultEncoding, + "text/html; q=0.8, text/yaml,application/json": "text/yaml", + "application/x-yaml; foo=bar": "application/x-yaml", + "text/monkey, TEXT/YAML ; zoom=zoom ": "text/yaml", + } + + for in, expects := range tests { + mt, enc := e.parseAccept(in) + if mt != expects { + t.Errorf("Expected %q, got %q", expects, mt) + continue + } + _, err := enc([]string{"hello", "world"}) + if err != nil { + t.Fatalf("Failed to marshal: %s", err) + } + } +} + +func TestTextMarshal(t *testing.T) { + tests := map[string]interface{}{ + "foo": "foo", + "5": 5, + "stinky cheese": errors.New("stinky cheese"), + } + for expect, in := range tests { + if o, err := textMarshal(in); err != nil || string(o) != expect { + t.Errorf("Expected %q, got %q", expect, o) + } + } + + if _, err := textMarshal(struct{ foo int }{5}); err != ErrUnsupportedKind { + t.Fatalf("Expected unsupported kind, got %v", err) + } +} + +func TestAcceptEncoder(t *testing.T) { + c := &Context{ + Encoder: &AcceptEncoder{DefaultEncoding: "application/json"}, + } + fn := func(w http.ResponseWriter, r *http.Request, c *Context) error { + c.Encoder.Encode(w, r, c, []string{"hello", "world"}) + return nil + } + s := httpHarness(c, "GET /", fn) + defer s.Close() + + res, err := http.Get(s.URL) + if err != nil { + t.Fatal(err) + } + if res.StatusCode != 200 { + t.Fatalf("Unexpected response code %d", res.StatusCode) + } + if mt := res.Header.Get("content-type"); mt != "application/json" { + t.Errorf("Unexpected content type: %q", mt) + } + + data, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatalf("Failed to read response body: %s", err) + } + + out := []string{} + if err := json.Unmarshal(data, &out); err != nil { + t.Fatalf("Failed to unmarshal JSON: %s", err) + } + + if out[0] != "hello" { + t.Fatalf("Unexpected JSON data in slot 0: %s", out[0]) + } +} diff --git a/cmd/manager/router/router.go b/cmd/manager/router/router.go new file mode 100644 index 000000000..6f15292cd --- /dev/null +++ b/cmd/manager/router/router.go @@ -0,0 +1,203 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* Package router is an HTTP router. + +This router provides appropriate dependency injection/encapsulation for the +HTTP routing layer. This removes the requirement to set global variables for +resources like database handles. + +This library does not replace the default HTTP mux because there is no need. +Instead, it implements an HTTP handler. + +It then defines a handler function that is given a context as well as a +request and response. +*/ +package router + +import ( + "fmt" + "net/http" + "os" + + "github.com/Masterminds/httputil" + "github.com/kubernetes/deployment-manager/cmd/manager/manager" + "github.com/kubernetes/deployment-manager/pkg/common" +) + +const LogAccess = "Access: %s %s" + +// Config holds the global configuration parameters passed into the router. +// +// Config is used concurrently. Once a config is created, it should be treated +// as immutable. +type Config struct { + // Address is the host and port (:8080) + Address string + // MaxTemplateLength is the maximum length of a template. + MaxTemplateLength int64 + // ExpanderName is the DNS name of the expansion service. + ExpanderName string + // ExpanderURL is the expander service's URL. + ExpanderURL string + // DeployerName is the deployer's DNS name + DeployerName string + // DeployerURL is the deployer's URL + DeployerURL string + // CredentialFile is the file to the credentials. + CredentialFile string + // CredentialSecrets tells the service to use a secrets file instead. + CredentialSecrets bool + // MongoName is the DNS name of the mongo server. + MongoName string + // MongoPort is the port for the MongoDB protocol on the mongo server. + // It is a string for historical reasons. + MongoPort string + // MongoAddress is the name and port. + MongoAddress string +} + +// Context contains dependencies that are passed to each handler function. +// +// Context carries typed information, often scoped to interfaces, so that the +// caller's contract with the service is known at compile time. +// +// Members of the context must be concurrency safe. +type Context struct { + Config *Config + // Manager is a deployment-manager/manager/manager.Manager + Manager manager.Manager + Encoder Encoder + CredentialProvider common.CredentialProvider +} + +func (c *Context) Log(msg string, v ...interface{}) { + // FIXME: This should be configurable via the context. + fmt.Fprintf(os.Stdout, msg+"\n", v...) +} + +func (c *Context) Err(msg string, v ...interface{}) { + // FIXME: This should be configurable via the context. + fmt.Fprintf(os.Stderr, msg+"\n", v...) +} + +// NotFound writes a 404 error to the client and logs an error. +func NotFound(w http.ResponseWriter, r *http.Request) { + // TODO: Log this. + w.WriteHeader(http.StatusNotFound) + fmt.Fprintln(w, "File Not Found") +} + +// Fatal writes a 500 response to the client and logs the message. +// +// Additional arguments are past into the the formatter as params to msg. +func Fatal(w http.ResponseWriter, r *http.Request, msg string, v ...interface{}) { + // TODO: Log this. + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintln(w, "Internal Server Error") +} + +// HandlerFunc responds to an individual HTTP request. +// +// Returned errors will be captured, logged, and returned as HTTP 500 errors. +type HandlerFunc func(w http.ResponseWriter, r *http.Request, c *Context) error + +// Handler implements an http.Handler. +// +// This is the top level route handler. +type Handler struct { + c *Context + resolver *httputil.Resolver + routes Routes +} + +// Create a new Handler. +// +// Routes cannot be modified after construction. The order that the route +// names are returned by Routes.Paths() determines the lookup order. +func NewHandler(c *Context, r Routes) *Handler { + paths := make([]string, r.Len()) + i := 0 + for _, k := range r.Paths() { + paths[i] = k + i++ + } + + return &Handler{ + c: c, + resolver: httputil.NewResolver(paths), + routes: r, + } +} + +// ServeHTTP serves an HTTP request. +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.c.Log(LogAccess, r.Method, r.URL) + route, err := h.resolver.Resolve(r) + if err != nil { + NotFound(w, r) + return + } + + fn, ok := h.routes.Get(route) + if !ok { + Fatal(w, r, "route %s missing", route) + } + + if err := fn(w, r, h.c); err != nil { + Fatal(w, r, err.Error()) + } +} + +// Routes defines a container for route-to-function mapping. +type Routes interface { + Add(string, HandlerFunc) + Get(string) (HandlerFunc, bool) + Len() int + Paths() []string +} + +// NewRoutes creates a default implementation of a Routes. +// +// The ordering of routes is nonderministic. +func NewRoutes() Routes { + return routeMap{} +} + +type routeMap map[string]HandlerFunc + +func (r routeMap) Add(name string, fn HandlerFunc) { + r[name] = fn +} + +func (r routeMap) Get(name string) (HandlerFunc, bool) { + f, ok := r[name] + return f, ok +} + +func (r routeMap) Len() int { + return len(r) +} + +func (r routeMap) Paths() []string { + b := make([]string, len(r)) + i := 0 + for k := range r { + b[i] = k + i++ + } + return b +} diff --git a/cmd/manager/router/router_test.go b/cmd/manager/router/router_test.go new file mode 100644 index 000000000..df673ad04 --- /dev/null +++ b/cmd/manager/router/router_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package router + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +// Canary +var v Routes = routeMap{} + +func TestHandler(t *testing.T) { + c := &Context{} + r := NewRoutes() + + r.Add("GET /", func(w http.ResponseWriter, r *http.Request, c *Context) error { + fmt.Fprintln(w, "hello") + return nil + }) + r.Add("POST /", func(w http.ResponseWriter, r *http.Request, c *Context) error { + fmt.Fprintln(w, "goodbye") + return nil + }) + + h := NewHandler(c, r) + + s := httptest.NewServer(h) + defer s.Close() + + res, err := http.Get(s.URL) + if err != nil { + t.Fatal(err) + } + data, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + + if "hello\n" != string(data) { + t.Errorf("Expected 'hello', got %q", data) + } +} + +// httpHarness is a simple test server fixture. +// Simple fixture for standing up a test server with a single route. +// +// You must Close() the returned server. +func httpHarness(c *Context, route string, fn HandlerFunc) *httptest.Server { + r := NewRoutes() + r.Add(route, fn) + h := NewHandler(c, r) + return httptest.NewServer(h) +} diff --git a/glide.lock b/glide.lock index 2a683ff75..67961e905 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: d9beab9a799ac8dd0d76c4f7a3a32753d44833dd3527b3caa8e786865ea26816 -updated: 2016-03-04T09:54:13.155442463-07:00 +hash: f34e830b237fcba5202f84c55b21764226b7e8c201f7622e9639c7a628e33b0b +updated: 2016-03-10T15:03:34.650809572-07:00 imports: - name: github.com/aokoli/goutils version: 9c37978a95bd5c709a15883b6242714ea6709e64 @@ -25,10 +25,12 @@ imports: version: 8f2758070a82adb7a3ad6b223a0b91878f32d400 - name: github.com/gorilla/mux version: 26a6070f849969ba72b72256e9f14cf519751690 +- name: github.com/Masterminds/httputil + version: e9b977e9cf16f9d339573e18f0f1f7ce5d3f419a - name: github.com/Masterminds/semver version: c4f7ef0702f269161a60489ccbbc9f1241ad1265 - name: github.com/Masterminds/sprig - version: fd057ca403105755181f84645696d705a58852dd + version: 0199893f008a87287bf2b4e3e390e66bb074c659 - name: golang.org/x/net version: 04b9de9b512f58addf28c9853d50ebef61c3953e subpackages: diff --git a/glide.yaml b/glide.yaml index 0ef3eafea..79237d249 100644 --- a/glide.yaml +++ b/glide.yaml @@ -14,3 +14,4 @@ import: - package: github.com/gorilla/mux - package: gopkg.in/yaml.v2 - package: github.com/Masterminds/sprig +- package: github.com/Masterminds/httputil