package client import ( "bytes" "context" "crypto/tls" "errors" "fmt" "io" "io/ioutil" "net" "net/http" "net/http/httputil" "os" "strconv" "strings" "sync" "time" ) type Client struct { sync.Mutex endpoint string username string password string auth bool httpClient *http.Client debugWriter io.Writer insecure bool } var ( ErrNotFound = errors.New("not found") ) type ClientOption func(*Client) func WithEndpoint(endpoint string) ClientOption { return func(client *Client) { client.endpoint = strings.TrimRight(endpoint, "/") } } func WithDebugWriter(debugWriter io.Writer) ClientOption { return func(client *Client) { client.debugWriter = debugWriter } } func WithHTTPClient(httpClient *http.Client) ClientOption { return func(client *Client) { client.httpClient = httpClient } } func WithInsecureClient(insecure bool) ClientOption { return func(client *Client) { client.insecure = insecure } } func WithCredentials(username, password string) ClientOption { return func(client *Client) { client.username = username client.password = password client.auth = true } } var ( ErrMissingEndpointEnv = errors.New("missing environment variable HAPROXY_LB_ENDPOINT") ErrMissingUserEnv = errors.New("missing environment variable HAPROXY_LB_USER") ErrMissingPasswordEnv = errors.New("missing environment variable HAPROXY_LB_PASSWORD") ErrMissingEndpoint = errors.New("missing Endpoint in config") ErrMissingUser = errors.New("missing User in config") ErrMissingPassword = errors.New("missing Password in config") ) type Config struct { Endpoint string `json:"endpoint"` NoAuth bool `json:"no_auth"` User string `json:"user"` Password string `json:"password"` Insecure bool `json:"insecure"` } func ClientOptionsFromConfig(cfg Config) (options []ClientOption, err error) { if cfg.Endpoint == "" { err = ErrMissingEndpoint return } options = []ClientOption{ WithEndpoint(cfg.Endpoint), WithInsecureClient(cfg.Insecure), } if cfg.NoAuth { return } if cfg.User == "" { err = ErrMissingUser return } if cfg.Password == "" { err = ErrMissingPassword return } options = append(options, WithCredentials(cfg.User, cfg.Password)) return } func ClientOptionsFromEnv() (options []ClientOption, err error) { endpoint := os.Getenv("HAPROXY_LB_ENDPOINT") if endpoint == "" { err = ErrMissingEndpointEnv return } insecure, _ := strconv.ParseBool(os.Getenv("HAPROXY_LB_INSECURE")) options = []ClientOption{ WithEndpoint(endpoint), WithInsecureClient(insecure), } noAuth, _ := strconv.ParseBool(os.Getenv("HAPROXY_LB_NO_AUTH")) if noAuth { return } user := os.Getenv("HAPROXY_LB_USER") if user == "" { err = ErrMissingUserEnv return } pass := os.Getenv("HAPROXY_LB_PASSWORD") if pass == "" { err = ErrMissingPasswordEnv return } options = append(options, WithCredentials(user, pass)) return } func NewClient(options ...ClientOption) *Client { client := &Client{} for _, option := range options { option(client) } if client.httpClient == nil { client.httpClient = &http.Client{Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, TLSClientConfig: &tls.Config{ InsecureSkipVerify: client.insecure, }, }} } return client } func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { url := c.endpoint + path req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } if c.auth { req.SetBasicAuth(c.username, c.password) } req = req.WithContext(ctx) return req, nil } func (c *Client) Do(r *http.Request, v interface{}) (resp *http.Response, err error) { if c.debugWriter != nil { dumpReq, err := httputil.DumpRequestOut(r, true) if err != nil { return &http.Response{}, err } fmt.Fprintf(c.debugWriter, "--- Request:\n%s\n\n", dumpReq) } resp, err = c.httpClient.Do(r) if err != nil { return } body, err := ioutil.ReadAll(resp.Body) if err != nil { resp.Body.Close() return } resp.Body.Close() resp.Body = ioutil.NopCloser(bytes.NewReader(body)) if c.debugWriter != nil { dumpResp, err := httputil.DumpResponse(resp, true) if err != nil { return resp, err } fmt.Fprintf(c.debugWriter, "--- Response:\n%s\n\n", dumpResp) } err = decodeResp(resp, v) return }