Skip to content

Commit

Permalink
Merge pull request #154 from Shopify/startup-config
Browse files Browse the repository at this point in the history
Add -config option for toxiproxy startup
  • Loading branch information
xthexder authored Dec 7, 2016
2 parents 8e40aad + 15c869e commit e17ee7d
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 51 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 42 additions & 49 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log"
"net"
"net/http"
"os"

"github.com/Shopify/toxiproxy/toxics"
"github.com/Sirupsen/logrus"
Expand All @@ -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")
Expand Down Expand Up @@ -132,65 +156,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)
Expand Down Expand Up @@ -453,3 +439,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
}
5 changes: 5 additions & 0 deletions cmd/toxiproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ import (

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)
}

func main() {
server := toxiproxy.NewServer()
if len(config) > 0 {
server.PopulateConfig(config)
}
server.Listen(host, port)
}
50 changes: 49 additions & 1 deletion proxy_collection.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit e17ee7d

Please sign in to comment.