// Package Toxiproxy provides a client wrapper around the Toxiproxy HTTP API for // testing the resiliency of Go applications. // // For use with Toxiproxy 2.x package toxiproxy import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "log" "net/http" "strings" ) // Client holds information about where to connect to Toxiproxy. type Client struct { endpoint string } type Attributes map[string]interface{} type Toxic struct { Name string `json:"name"` Type string `json:"type"` Stream string `json:"stream,omitempty"` Toxicity float32 `json:"toxicity"` Attributes Attributes `json:"attributes"` } type Toxics []Toxic type Proxy struct { Name string `json:"name"` // The name of the proxy Listen string `json:"listen"` // The address the proxy listens on Upstream string `json:"upstream"` // The upstream address to proxy to Enabled bool `json:"enabled"` // Whether the proxy is enabled ActiveToxics Toxics `json:"toxics"` // The toxics active on this proxy client *Client created bool // True if this proxy exists on the server } // NewClient creates a new client which provides the base of all communication // with Toxiproxy. Endpoint is the address to the proxy (e.g. localhost:8474 if // not overriden) func NewClient(endpoint string) *Client { if strings.HasPrefix(endpoint, "https://") { log.Fatal("the toxiproxy client does not support https") } else if !strings.HasPrefix(endpoint, "http://") { endpoint = "http://" + endpoint } return &Client{endpoint: endpoint} } // Proxies returns a map with all the proxies and their toxics. func (client *Client) Proxies() (map[string]*Proxy, error) { resp, err := http.Get(client.endpoint + "/proxies") if err != nil { return nil, err } err = checkError(resp, http.StatusOK, "Proxies") if err != nil { return nil, err } proxies := make(map[string]*Proxy) err = json.NewDecoder(resp.Body).Decode(&proxies) if err != nil { return nil, err } for _, proxy := range proxies { proxy.client = client proxy.created = true } return proxies, nil } // Generates a new uncommitted proxy instance. In order to use the result, the // proxy fields will need to be set and have `Save()` called. func (client *Client) NewProxy() *Proxy { return &Proxy{ client: client, } } // CreateProxy instantiates a new proxy and starts listening on the specified address. // This is an alias for `NewProxy()` + `proxy.Save()` func (client *Client) CreateProxy(name, listen, upstream string) (*Proxy, error) { proxy := &Proxy{ Name: name, Listen: listen, Upstream: upstream, Enabled: true, client: client, } err := proxy.Save() if err != nil { return nil, err } return proxy, nil } // Proxy returns a proxy by name. func (client *Client) Proxy(name string) (*Proxy, error) { // TODO url encode resp, err := http.Get(client.endpoint + "/proxies/" + name) if err != nil { return nil, err } err = checkError(resp, http.StatusOK, "Proxy") if err != nil { return nil, err } proxy := new(Proxy) err = json.NewDecoder(resp.Body).Decode(proxy) if err != nil { return nil, err } proxy.client = client proxy.created = true return proxy, nil } // Create a list of proxies using a configuration list. If a proxy already exists, it will be replaced // with the specified configuration. For large amounts of proxies, `config` can be loaded from a file. // Returns a list of the successfully created proxies. func (client *Client) Populate(config []Proxy) ([]*Proxy, error) { proxies := struct { Proxies []*Proxy `json:"proxies"` }{} request, err := json.Marshal(config) if err != nil { return nil, err } resp, err := http.Post(client.endpoint+"/populate", "application/json", bytes.NewReader(request)) if err != nil { return nil, err } // Response body may need to be read twice, we want to return both the proxy list and any errors var body bytes.Buffer tee := io.TeeReader(resp.Body, &body) err = json.NewDecoder(tee).Decode(&proxies) if err != nil { return nil, err } resp.Body = ioutil.NopCloser(&body) err = checkError(resp, http.StatusCreated, "Populate") return proxies.Proxies, err } // Save saves changes to a proxy such as its enabled status or upstream port. func (proxy *Proxy) Save() error { request, err := json.Marshal(proxy) if err != nil { return err } var resp *http.Response if proxy.created { resp, err = http.Post(proxy.client.endpoint+"/proxies/"+proxy.Name, "text/plain", bytes.NewReader(request)) } else { resp, err = http.Post(proxy.client.endpoint+"/proxies", "application/json", bytes.NewReader(request)) } if err != nil { return err } if proxy.created { err = checkError(resp, http.StatusOK, "Save") } else { err = checkError(resp, http.StatusCreated, "Create") } if err != nil { return err } err = json.NewDecoder(resp.Body).Decode(proxy) if err != nil { return err } proxy.created = true return nil } // Enable a proxy again after it has been disabled. func (proxy *Proxy) Enable() error { proxy.Enabled = true return proxy.Save() } // Disable a proxy so that no connections can pass through. This will drop all active connections. func (proxy *Proxy) Disable() error { proxy.Enabled = false return proxy.Save() } // Delete a proxy complete and close all existing connections through it. All information about // the proxy such as listen port and active toxics will be deleted as well. If you just wish to // stop and later enable a proxy, use `Enable()` and `Disable()`. func (proxy *Proxy) Delete() error { httpClient := &http.Client{} req, err := http.NewRequest("DELETE", proxy.client.endpoint+"/proxies/"+proxy.Name, nil) if err != nil { return err } resp, err := httpClient.Do(req) if err != nil { return err } return checkError(resp, http.StatusNoContent, "Delete") } // Toxics returns a map of all the active toxics and their attributes. func (proxy *Proxy) Toxics() (Toxics, error) { resp, err := http.Get(proxy.client.endpoint + "/proxies/" + proxy.Name + "/toxics") if err != nil { return nil, err } err = checkError(resp, http.StatusOK, "Toxics") if err != nil { return nil, err } toxics := make(Toxics, 0) err = json.NewDecoder(resp.Body).Decode(&toxics) if err != nil { return nil, err } return toxics, nil } // AddToxic adds a toxic to the given stream direction. // If a name is not specified, it will default to _. // If a stream is not specified, it will default to downstream. // See https://github.com/Shopify/toxiproxy#toxics for a list of all Toxic types. func (proxy *Proxy) AddToxic(name, typeName, stream string, toxicity float32, attrs Attributes) (*Toxic, error) { toxic := Toxic{name, typeName, stream, toxicity, attrs} if toxic.Toxicity == -1 { toxic.Toxicity = 1 // Just to be consistent with a toxicity of -1 using the default } request, err := json.Marshal(&toxic) if err != nil { return nil, err } resp, err := http.Post(proxy.client.endpoint+"/proxies/"+proxy.Name+"/toxics", "application/json", bytes.NewReader(request)) if err != nil { return nil, err } err = checkError(resp, http.StatusOK, "AddToxic") if err != nil { return nil, err } result := &Toxic{} err = json.NewDecoder(resp.Body).Decode(result) if err != nil { return nil, err } return result, nil } // UpdateToxic sets the parameters for an existing toxic with the given name. // If toxicity is set to -1, the current value will be used. func (proxy *Proxy) UpdateToxic(name string, toxicity float32, attrs Attributes) (*Toxic, error) { toxic := map[string]interface{}{ "attributes": attrs, } if toxicity != -1 { toxic["toxicity"] = toxicity } request, err := json.Marshal(&toxic) if err != nil { return nil, err } resp, err := http.Post(proxy.client.endpoint+"/proxies/"+proxy.Name+"/toxics/"+name, "application/json", bytes.NewReader(request)) if err != nil { return nil, err } err = checkError(resp, http.StatusOK, "UpdateToxic") if err != nil { return nil, err } result := &Toxic{} err = json.NewDecoder(resp.Body).Decode(result) if err != nil { return nil, err } return result, nil } // RemoveToxic renives the toxic with the given name. func (proxy *Proxy) RemoveToxic(name string) error { httpClient := &http.Client{} req, err := http.NewRequest("DELETE", proxy.client.endpoint+"/proxies/"+proxy.Name+"/toxics/"+name, nil) if err != nil { return err } resp, err := httpClient.Do(req) if err != nil { return err } return checkError(resp, http.StatusNoContent, "RemoveToxic") } // ResetState resets the state of all proxies and toxics in Toxiproxy. func (client *Client) ResetState() error { resp, err := http.Post(client.endpoint+"/reset", "text/plain", bytes.NewReader([]byte{})) if err != nil { return err } return checkError(resp, http.StatusNoContent, "ResetState") } type ApiError struct { Message string `json:"error"` Status int `json:"status"` } func (err *ApiError) Error() string { return fmt.Sprintf("HTTP %d: %s", err.Status, err.Message) } func checkError(resp *http.Response, expectedCode int, caller string) error { if resp.StatusCode != expectedCode { apiError := new(ApiError) err := json.NewDecoder(resp.Body).Decode(apiError) if err != nil { apiError.Message = fmt.Sprintf("Unexpected response code, expected %d", expectedCode) apiError.Status = resp.StatusCode } return fmt.Errorf("%s: %v", caller, apiError) } return nil }