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 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
|
// 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"
"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
// Proxy represents a Proxy.
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, "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.
func (client *Client) Populate(config []Proxy) (map[string]*Proxy, error) {
proxies := make(map[string]*Proxy, len(config))
for _, proxy := range config {
existing, err := client.Proxy(proxy.Name)
if err != nil && err.Error() != "Proxy: HTTP 404: proxy not found" {
return nil, err
} else if existing != nil && (existing.Listen != proxy.Listen || existing.Upstream != proxy.Upstream) {
existing.Delete()
}
proxies[proxy.Name], err = client.CreateProxy(proxy.Name, proxy.Listen, proxy.Upstream)
if err != nil {
return nil, err
}
}
return proxies, nil
}
// 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 <type>_<stream>.
// 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 {
Title string `json:"title"`
Status int `json:"status"`
}
func (err *ApiError) Error() string {
return fmt.Sprintf("HTTP %d: %s", err.Status, err.Title)
}
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.Title = fmt.Sprintf("Unexpected response code, expected %d", expectedCode)
apiError.Status = resp.StatusCode
}
return fmt.Errorf("%s: %v", caller, apiError)
}
return nil
}
|