feat: 测试交付 Q1 混沌压测骨架与 Q4 镜像骨架
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
// Package chaos 提供基于 toxiproxy 的弱网故障注入辅助。
|
||||
// 上游地址由调用方传入,不写死端口。
|
||||
package chaos
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client 调用 toxiproxy HTTP API。
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient 创建客户端。baseURL 形如 http://127.0.0.1:8474。
|
||||
func NewClient(baseURL string) *Client {
|
||||
return &Client{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy 描述一个 toxiproxy 代理。
|
||||
type Proxy struct {
|
||||
Name string `json:"name"`
|
||||
Listen string `json:"listen"`
|
||||
Upstream string `json:"upstream"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// Toxic 描述一条故障规则。
|
||||
type Toxic struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Stream string `json:"stream,omitempty"`
|
||||
Toxicity float32 `json:"toxicity,omitempty"`
|
||||
Attributes map[string]any `json:"attributes,omitempty"`
|
||||
}
|
||||
|
||||
// CreateProxy 创建或覆盖同名代理。listen 可用 host:0 让 toxiproxy 分配端口。
|
||||
func (c *Client) CreateProxy(name, listen, upstream string) (*Proxy, error) {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("proxy name required")
|
||||
}
|
||||
if listen == "" {
|
||||
return nil, fmt.Errorf("listen required")
|
||||
}
|
||||
if upstream == "" {
|
||||
return nil, fmt.Errorf("upstream required")
|
||||
}
|
||||
body := Proxy{
|
||||
Name: name,
|
||||
Listen: listen,
|
||||
Upstream: upstream,
|
||||
Enabled: true,
|
||||
}
|
||||
var out Proxy
|
||||
if err := c.doJSON(http.MethodPost, "/proxies", body, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// DeleteProxy 删除代理;不存在时忽略。
|
||||
func (c *Client) DeleteProxy(name string) error {
|
||||
req, err := http.NewRequest(http.MethodDelete, c.BaseURL+"/proxies/"+name, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("delete proxy: %s: %s", resp.Status, strings.TrimSpace(string(b)))
|
||||
}
|
||||
|
||||
// GetProxy 读取代理(含实际 listen 地址)。
|
||||
func (c *Client) GetProxy(name string) (*Proxy, error) {
|
||||
var out Proxy
|
||||
if err := c.doJSON(http.MethodGet, "/proxies/"+name, nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// AddLatency 注入下行/上行延迟(毫秒)。stream 为空时默认 downstream。
|
||||
func (c *Client) AddLatency(proxyName, toxicName string, latencyMs, jitterMs int, stream string) (*Toxic, error) {
|
||||
if stream == "" {
|
||||
stream = "downstream"
|
||||
}
|
||||
t := Toxic{
|
||||
Name: toxicName,
|
||||
Type: "latency",
|
||||
Stream: stream,
|
||||
Toxicity: 1,
|
||||
Attributes: map[string]any{
|
||||
"latency": latencyMs,
|
||||
"jitter": jitterMs,
|
||||
},
|
||||
}
|
||||
return c.addToxic(proxyName, t)
|
||||
}
|
||||
|
||||
// AddResetPeer 在连接上注入 TCP RST(断开)。timeoutMs 为触发前等待。
|
||||
func (c *Client) AddResetPeer(proxyName, toxicName string, timeoutMs int, stream string) (*Toxic, error) {
|
||||
if stream == "" {
|
||||
stream = "downstream"
|
||||
}
|
||||
t := Toxic{
|
||||
Name: toxicName,
|
||||
Type: "reset_peer",
|
||||
Stream: stream,
|
||||
Toxicity: 1,
|
||||
Attributes: map[string]any{
|
||||
"timeout": timeoutMs,
|
||||
},
|
||||
}
|
||||
return c.addToxic(proxyName, t)
|
||||
}
|
||||
|
||||
// RemoveToxic 删除一条 toxic。
|
||||
func (c *Client) RemoveToxic(proxyName, toxicName string) error {
|
||||
req, err := http.NewRequest(http.MethodDelete, c.BaseURL+"/proxies/"+proxyName+"/toxics/"+toxicName, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("remove toxic: %s: %s", resp.Status, strings.TrimSpace(string(b)))
|
||||
}
|
||||
|
||||
func (c *Client) addToxic(proxyName string, t Toxic) (*Toxic, error) {
|
||||
var out Toxic
|
||||
if err := c.doJSON(http.MethodPost, "/proxies/"+proxyName+"/toxics", t, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (c *Client) doJSON(method, path string, in any, out any) error {
|
||||
var body io.Reader
|
||||
if in != nil {
|
||||
b, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = bytes.NewReader(b)
|
||||
}
|
||||
req, err := http.NewRequest(method, c.BaseURL+path, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if in != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("%s %s: %s: %s", method, path, resp.Status, strings.TrimSpace(string(respBody)))
|
||||
}
|
||||
if out == nil || len(respBody) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(respBody, out)
|
||||
}
|
||||
Reference in New Issue
Block a user