From 46df58de01ccec4756ef3495aa1190eb1ea4dc49 Mon Sep 17 00:00:00 2001 From: Jacob Wirth Date: Wed, 7 Dec 2016 13:51:22 -0500 Subject: [PATCH 1/3] Add -config option for toxiproxy startup --- api.go | 67 ++++++++++++--------------------------------- cmd/toxiproxy.go | 26 ++++++++++++++++++ proxy_collection.go | 50 ++++++++++++++++++++++++++++++++- 3 files changed, 93 insertions(+), 50 deletions(-) diff --git a/api.go b/api.go index c1b2f3a4..04f26eff 100644 --- a/api.go +++ b/api.go @@ -132,65 +132,27 @@ func (server *ApiServer) ProxyCreate(response http.ResponseWriter, request *http } func (server *ApiServer) Populate(response http.ResponseWriter, request *http.Request) { - input := []struct { - Proxy - Enabled *bool `json:"enabled"` // Overrides Proxy field to make field nullable - }{} + proxies, err := server.Collection.PopulateJson(request.Body) - err := json.NewDecoder(request.Body).Decode(&input) - if apiError(response, joinError(err, ErrBadRequestBody)) { - return - } - - // Check for valid input before creating any proxies - t := true - for i, p := range input { - if len(p.Name) < 1 { - apiError(response, joinError(fmt.Errorf("name at proxy %d", i+1), ErrMissingField)) - return - } - if len(p.Upstream) < 1 { - apiError(response, joinError(fmt.Errorf("upstream at proxy %d", i+1), ErrMissingField)) - return - } - if p.Enabled == nil { - input[i].Enabled = &t - } - } - - proxies := make([]proxyToxics, 0, len(input)) - - responseCode := http.StatusCreated - var apiErr *ApiError - for _, p := range input { - proxy := NewProxy() - proxy.Name = p.Name - proxy.Listen = p.Listen - proxy.Upstream = p.Upstream - - err = server.Collection.AddOrReplace(proxy, *p.Enabled) - if err != nil { - var ok bool - apiErr, ok = err.(*ApiError) - if !ok { - logrus.Warn("Error did not include status code: ", err) - apiErr = &ApiError{err.Error(), http.StatusInternalServerError} - } - responseCode = apiErr.StatusCode - break - } - - proxies = append(proxies, proxyWithToxics(proxy)) + apiErr, ok := err.(*ApiError) + if !ok && err != nil { + logrus.Warn("Error did not include status code: ", err) + apiErr = &ApiError{err.Error(), http.StatusInternalServerError} } data, err := json.Marshal(struct { *ApiError `json:",omitempty"` Proxies []proxyToxics `json:"proxies"` - }{apiErr, proxies}) + }{apiErr, proxiesWithToxics(proxies)}) if apiError(response, err) { return } + responseCode := http.StatusCreated + if apiErr != nil { + responseCode = apiErr.StatusCode + } + response.Header().Set("Content-Type", "application/json") response.WriteHeader(responseCode) _, err = response.Write(data) @@ -453,3 +415,10 @@ func proxyWithToxics(proxy *Proxy) (result proxyToxics) { result.Toxics = proxy.Toxics.GetToxicArray() return } + +func proxiesWithToxics(proxies []*Proxy) (result []proxyToxics) { + for _, proxy := range proxies { + result = append(result, proxyWithToxics(proxy)) + } + return +} diff --git a/cmd/toxiproxy.go b/cmd/toxiproxy.go index 4ae6a776..672b4a2c 100644 --- a/cmd/toxiproxy.go +++ b/cmd/toxiproxy.go @@ -3,17 +3,21 @@ package main import ( "flag" "math/rand" + "os" "time" "github.com/Shopify/toxiproxy" + "github.com/Sirupsen/logrus" ) var host string var port string +var config string func init() { flag.StringVar(&host, "host", "localhost", "Host for toxiproxy's API to listen on") flag.StringVar(&port, "port", "8474", "Port for toxiproxy's API to listen on") + flag.StringVar(&config, "config", "", "JSON file containing proxies to create on startup") seed := flag.Int64("seed", time.Now().UTC().UnixNano(), "Seed for randomizing toxics with") flag.Parse() rand.Seed(*seed) @@ -21,5 +25,27 @@ func init() { func main() { server := toxiproxy.NewServer() + if len(config) > 0 { + file, err := os.Open(config) + if err != nil { + logrus.WithFields(logrus.Fields{ + "config": config, + "error": err, + }).Error("Error reading config file") + } else { + proxies, err := server.Collection.PopulateJson(file) + if err != nil { + logrus.WithFields(logrus.Fields{ + "config": config, + "error": err, + }).Error("Failed to populate proxies from file") + } else { + logrus.WithFields(logrus.Fields{ + "config": config, + "proxies": len(proxies), + }).Info("Populated proxies from file") + } + } + } server.Listen(host, port) } diff --git a/proxy_collection.go b/proxy_collection.go index f9ab05aa..3070f5e7 100644 --- a/proxy_collection.go +++ b/proxy_collection.go @@ -1,6 +1,11 @@ package toxiproxy -import "sync" +import ( + "encoding/json" + "fmt" + "io" + "sync" +) // ProxyCollection is a collection of proxies. It's the interface for anything // to add and remove proxies from the toxiproxy instance. It's responsibilty is @@ -61,6 +66,49 @@ func (collection *ProxyCollection) AddOrReplace(proxy *Proxy, start bool) error return nil } +func (collection *ProxyCollection) PopulateJson(data io.Reader) ([]*Proxy, error) { + input := []struct { + Proxy + Enabled *bool `json:"enabled"` // Overrides Proxy field to make field nullable + }{} + + err := json.NewDecoder(data).Decode(&input) + if err != nil { + return nil, joinError(err, ErrBadRequestBody) + } + + // Check for valid input before creating any proxies + t := true + for i, p := range input { + if len(p.Name) < 1 { + return nil, joinError(fmt.Errorf("name at proxy %d", i+1), ErrMissingField) + } + if len(p.Upstream) < 1 { + return nil, joinError(fmt.Errorf("upstream at proxy %d", i+1), ErrMissingField) + } + if p.Enabled == nil { + input[i].Enabled = &t + } + } + + proxies := make([]*Proxy, 0, len(input)) + + for _, p := range input { + proxy := NewProxy() + proxy.Name = p.Name + proxy.Listen = p.Listen + proxy.Upstream = p.Upstream + + err = collection.AddOrReplace(proxy, *p.Enabled) + if err != nil { + break + } + + proxies = append(proxies, proxy) + } + return proxies, err +} + func (collection *ProxyCollection) Proxies() map[string]*Proxy { collection.RLock() defer collection.RUnlock() From e233a65879e223df709d7d41bc1ea2cd01b39d48 Mon Sep 17 00:00:00 2001 From: Jacob Wirth Date: Wed, 7 Dec 2016 14:21:07 -0500 Subject: [PATCH 2/3] Update docs --- CHANGELOG.md | 1 + README.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 599fd9eb..fdacd764 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 2.1.0 (Unreleased) +* Add -config server option to populate on startup #154 * Updated CLI for scriptability #133 * Add `/populate` endpoint to server #111 * Change error responses from `title` to `error` diff --git a/README.md b/README.md index 5dd8fb93..556cce37 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,9 @@ This makes sure there are no clashes between applications using the same Toxiproxy. For large application we recommend storing the Toxiproxy configurations in a -separate configuration file. We use `config/toxiproxy.json`. +separate configuration file. We use `config/toxiproxy.json`. This file can be +passed to the server using the `-config` option, or loaded by the application +to use with the `populate` function. Use ports outside the ephemeral port range to avoid random port conflicts. It's `32,768` to `61,000` on Linux by default, see From 15c869e9b083d3850e47291dca1596163ed83393 Mon Sep 17 00:00:00 2001 From: Jacob Wirth Date: Wed, 7 Dec 2016 14:59:28 -0500 Subject: [PATCH 3/3] Clean up main() --- api.go | 24 ++++++++++++++++++++++++ cmd/toxiproxy.go | 23 +---------------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/api.go b/api.go index 04f26eff..25748c3a 100644 --- a/api.go +++ b/api.go @@ -6,6 +6,7 @@ import ( "log" "net" "net/http" + "os" "github.com/Shopify/toxiproxy/toxics" "github.com/Sirupsen/logrus" @@ -22,6 +23,29 @@ func NewServer() *ApiServer { } } +func (server *ApiServer) PopulateConfig(filename string) { + file, err := os.Open(filename) + if err != nil { + logrus.WithFields(logrus.Fields{ + "config": filename, + "error": err, + }).Error("Error reading config file") + } else { + proxies, err := server.Collection.PopulateJson(file) + if err != nil { + logrus.WithFields(logrus.Fields{ + "config": filename, + "error": err, + }).Error("Failed to populate proxies from file") + } else { + logrus.WithFields(logrus.Fields{ + "config": filename, + "proxies": len(proxies), + }).Info("Populated proxies from file") + } + } +} + func (server *ApiServer) Listen(host string, port string) { r := mux.NewRouter() r.HandleFunc("/reset", server.ResetState).Methods("POST") diff --git a/cmd/toxiproxy.go b/cmd/toxiproxy.go index 672b4a2c..e53a8fb7 100644 --- a/cmd/toxiproxy.go +++ b/cmd/toxiproxy.go @@ -3,11 +3,9 @@ package main import ( "flag" "math/rand" - "os" "time" "github.com/Shopify/toxiproxy" - "github.com/Sirupsen/logrus" ) var host string @@ -26,26 +24,7 @@ func init() { func main() { server := toxiproxy.NewServer() if len(config) > 0 { - file, err := os.Open(config) - if err != nil { - logrus.WithFields(logrus.Fields{ - "config": config, - "error": err, - }).Error("Error reading config file") - } else { - proxies, err := server.Collection.PopulateJson(file) - if err != nil { - logrus.WithFields(logrus.Fields{ - "config": config, - "error": err, - }).Error("Failed to populate proxies from file") - } else { - logrus.WithFields(logrus.Fields{ - "config": config, - "proxies": len(proxies), - }).Info("Populated proxies from file") - } - } + server.PopulateConfig(config) } server.Listen(host, port) }