github.com/thanos-io/thanos@v0.32.5/pkg/httpconfig/http.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  // Package httpconfig is a wrapper around github.com/prometheus/common/config.
     5  package httpconfig
     6  
     7  import (
     8  	"context"
     9  	"crypto/tls"
    10  	"fmt"
    11  	"net/http"
    12  	"net/url"
    13  	"path"
    14  	"sync"
    15  	"time"
    16  
    17  	extpromhttp "github.com/thanos-io/thanos/pkg/extprom/http"
    18  
    19  	"github.com/go-kit/log"
    20  	"github.com/mwitkow/go-conntrack"
    21  	config_util "github.com/prometheus/common/config"
    22  	"github.com/prometheus/common/model"
    23  	"github.com/prometheus/common/version"
    24  	"github.com/prometheus/prometheus/discovery/file"
    25  	"github.com/prometheus/prometheus/discovery/targetgroup"
    26  	"golang.org/x/net/http2"
    27  	"gopkg.in/yaml.v2"
    28  
    29  	"github.com/thanos-io/thanos/pkg/discovery/cache"
    30  )
    31  
    32  // ClientConfig configures an HTTP client.
    33  type ClientConfig struct {
    34  	// The HTTP basic authentication credentials for the targets.
    35  	BasicAuth BasicAuth `yaml:"basic_auth"`
    36  	// The bearer token for the targets.
    37  	BearerToken string `yaml:"bearer_token"`
    38  	// The bearer token file for the targets.
    39  	BearerTokenFile string `yaml:"bearer_token_file"`
    40  	// HTTP proxy server to use to connect to the targets.
    41  	ProxyURL string `yaml:"proxy_url"`
    42  	// TLSConfig to use to connect to the targets.
    43  	TLSConfig TLSConfig `yaml:"tls_config"`
    44  	// TransportConfig for Client transport properties
    45  	TransportConfig TransportConfig `yaml:"transport_config"`
    46  	// ClientMetrics contains metrics that will be used to instrument
    47  	// the client that will be created with this config.
    48  	ClientMetrics *extpromhttp.ClientMetrics `yaml:"-"`
    49  }
    50  
    51  // TLSConfig configures TLS connections.
    52  type TLSConfig struct {
    53  	// The CA cert to use for the targets.
    54  	CAFile string `yaml:"ca_file"`
    55  	// The client cert file for the targets.
    56  	CertFile string `yaml:"cert_file"`
    57  	// The client key file for the targets.
    58  	KeyFile string `yaml:"key_file"`
    59  	// Used to verify the hostname for the targets. See https://tools.ietf.org/html/rfc4366#section-3.1
    60  	ServerName string `yaml:"server_name"`
    61  	// Disable target certificate validation.
    62  	InsecureSkipVerify bool `yaml:"insecure_skip_verify"`
    63  }
    64  
    65  // BasicAuth configures basic authentication for HTTP clients.
    66  type BasicAuth struct {
    67  	Username     string `yaml:"username"`
    68  	Password     string `yaml:"password"`
    69  	PasswordFile string `yaml:"password_file"`
    70  }
    71  
    72  // IsZero returns false if basic authentication isn't enabled.
    73  func (b BasicAuth) IsZero() bool {
    74  	return b.Username == "" && b.Password == "" && b.PasswordFile == ""
    75  }
    76  
    77  // Transport configures client's transport properties.
    78  type TransportConfig struct {
    79  	MaxIdleConns          int   `yaml:"max_idle_conns"`
    80  	MaxIdleConnsPerHost   int   `yaml:"max_idle_conns_per_host"`
    81  	IdleConnTimeout       int64 `yaml:"idle_conn_timeout"`
    82  	ResponseHeaderTimeout int64 `yaml:"response_header_timeout"`
    83  	ExpectContinueTimeout int64 `yaml:"expect_continue_timeout"`
    84  	MaxConnsPerHost       int   `yaml:"max_conns_per_host"`
    85  	DisableCompression    bool  `yaml:"disable_compression"`
    86  	TLSHandshakeTimeout   int64 `yaml:"tls_handshake_timeout"`
    87  }
    88  
    89  var defaultTransportConfig TransportConfig = TransportConfig{
    90  	MaxIdleConns:          100,
    91  	MaxIdleConnsPerHost:   2,
    92  	ResponseHeaderTimeout: 0,
    93  	MaxConnsPerHost:       0,
    94  	IdleConnTimeout:       int64(90 * time.Second),
    95  	ExpectContinueTimeout: int64(10 * time.Second),
    96  	DisableCompression:    false,
    97  	TLSHandshakeTimeout:   int64(10 * time.Second),
    98  }
    99  
   100  func NewDefaultClientConfig() ClientConfig {
   101  	return ClientConfig{TransportConfig: defaultTransportConfig}
   102  }
   103  
   104  func NewClientConfigFromYAML(cfg []byte) (*ClientConfig, error) {
   105  	conf := &ClientConfig{TransportConfig: defaultTransportConfig}
   106  	if err := yaml.Unmarshal(cfg, conf); err != nil {
   107  		return nil, err
   108  	}
   109  	return conf, nil
   110  }
   111  
   112  // NewRoundTripperFromConfig returns a new HTTP RoundTripper configured for the
   113  // given http.HTTPClientConfig and http.HTTPClientOption.
   114  func NewRoundTripperFromConfig(cfg config_util.HTTPClientConfig, transportConfig TransportConfig, name string) (http.RoundTripper, error) {
   115  	newRT := func(tlsConfig *tls.Config) (http.RoundTripper, error) {
   116  		var rt http.RoundTripper = &http.Transport{
   117  			Proxy:                 http.ProxyURL(cfg.ProxyURL.URL),
   118  			MaxIdleConns:          transportConfig.MaxIdleConns,
   119  			MaxIdleConnsPerHost:   transportConfig.MaxIdleConnsPerHost,
   120  			MaxConnsPerHost:       transportConfig.MaxConnsPerHost,
   121  			TLSClientConfig:       tlsConfig,
   122  			DisableCompression:    transportConfig.DisableCompression,
   123  			IdleConnTimeout:       time.Duration(transportConfig.IdleConnTimeout),
   124  			ResponseHeaderTimeout: time.Duration(transportConfig.ResponseHeaderTimeout),
   125  			ExpectContinueTimeout: time.Duration(transportConfig.ExpectContinueTimeout),
   126  			TLSHandshakeTimeout:   time.Duration(transportConfig.TLSHandshakeTimeout),
   127  			DialContext: conntrack.NewDialContextFunc(
   128  				conntrack.DialWithTracing(),
   129  				conntrack.DialWithName(name)),
   130  		}
   131  
   132  		// HTTP/2 support is golang has many problematic cornercases where
   133  		// dead connections would be kept and used in connection pools.
   134  		// https://github.com/golang/go/issues/32388
   135  		// https://github.com/golang/go/issues/39337
   136  		// https://github.com/golang/go/issues/39750
   137  		// TODO: Re-Enable HTTP/2 once upstream issue is fixed.
   138  		// TODO: use ForceAttemptHTTP2 when we move to Go 1.13+.
   139  		err := http2.ConfigureTransport(rt.(*http.Transport))
   140  		if err != nil {
   141  			return nil, err
   142  		}
   143  
   144  		// If an authorization_credentials is provided, create a round tripper that will set the
   145  		// Authorization header correctly on each request.
   146  		if cfg.Authorization != nil && len(cfg.Authorization.Credentials) > 0 {
   147  			rt = config_util.NewAuthorizationCredentialsRoundTripper(cfg.Authorization.Type, cfg.Authorization.Credentials, rt)
   148  		} else if cfg.Authorization != nil && len(cfg.Authorization.CredentialsFile) > 0 {
   149  			rt = config_util.NewAuthorizationCredentialsFileRoundTripper(cfg.Authorization.Type, cfg.Authorization.CredentialsFile, rt)
   150  		}
   151  		// Backwards compatibility, be nice with importers who would not have
   152  		// called Validate().
   153  		if len(cfg.BearerToken) > 0 {
   154  			rt = config_util.NewAuthorizationCredentialsRoundTripper("Bearer", cfg.BearerToken, rt)
   155  		} else if len(cfg.BearerTokenFile) > 0 {
   156  			rt = config_util.NewAuthorizationCredentialsFileRoundTripper("Bearer", cfg.BearerTokenFile, rt)
   157  		}
   158  
   159  		if cfg.BasicAuth != nil {
   160  			rt = config_util.NewBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, cfg.BasicAuth.PasswordFile, rt)
   161  		}
   162  		// Return a new configured RoundTripper.
   163  		return rt, nil
   164  	}
   165  
   166  	tlsConfig, err := config_util.NewTLSConfig(&cfg.TLSConfig)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  
   171  	if len(cfg.TLSConfig.CAFile) == 0 {
   172  		// No need for a RoundTripper that reloads the CA file automatically.
   173  		return newRT(tlsConfig)
   174  	}
   175  
   176  	return config_util.NewTLSRoundTripper(tlsConfig, config_util.TLSRoundTripperSettings{
   177  		CAFile:   cfg.TLSConfig.CAFile,
   178  		CertFile: cfg.TLSConfig.CertFile,
   179  		KeyFile:  cfg.TLSConfig.KeyFile,
   180  	}, newRT)
   181  }
   182  
   183  // NewHTTPClient returns a new HTTP client.
   184  func NewHTTPClient(cfg ClientConfig, name string) (*http.Client, error) {
   185  	httpClientConfig := config_util.HTTPClientConfig{
   186  		BearerToken:     config_util.Secret(cfg.BearerToken),
   187  		BearerTokenFile: cfg.BearerTokenFile,
   188  		TLSConfig: config_util.TLSConfig{
   189  			CAFile:             cfg.TLSConfig.CAFile,
   190  			CertFile:           cfg.TLSConfig.CertFile,
   191  			KeyFile:            cfg.TLSConfig.KeyFile,
   192  			ServerName:         cfg.TLSConfig.ServerName,
   193  			InsecureSkipVerify: cfg.TLSConfig.InsecureSkipVerify,
   194  		},
   195  	}
   196  	if cfg.ProxyURL != "" {
   197  		var proxy config_util.URL
   198  		err := yaml.Unmarshal([]byte(cfg.ProxyURL), &proxy)
   199  		if err != nil {
   200  			return nil, err
   201  		}
   202  		httpClientConfig.ProxyURL = proxy
   203  	}
   204  	if !cfg.BasicAuth.IsZero() {
   205  		httpClientConfig.BasicAuth = &config_util.BasicAuth{
   206  			Username:     cfg.BasicAuth.Username,
   207  			Password:     config_util.Secret(cfg.BasicAuth.Password),
   208  			PasswordFile: cfg.BasicAuth.PasswordFile,
   209  		}
   210  	}
   211  
   212  	if cfg.BearerToken != "" {
   213  		httpClientConfig.BearerToken = config_util.Secret(cfg.BearerToken)
   214  	}
   215  
   216  	if cfg.BearerTokenFile != "" {
   217  		httpClientConfig.BearerTokenFile = cfg.BearerTokenFile
   218  	}
   219  
   220  	if err := httpClientConfig.Validate(); err != nil {
   221  		return nil, err
   222  	}
   223  
   224  	rt, err := NewRoundTripperFromConfig(
   225  		httpClientConfig,
   226  		cfg.TransportConfig,
   227  		name,
   228  	)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	if cfg.ClientMetrics != nil {
   234  		rt = extpromhttp.InstrumentedRoundTripper(rt, cfg.ClientMetrics)
   235  	}
   236  
   237  	rt = &userAgentRoundTripper{name: ThanosUserAgent, rt: rt}
   238  	client := &http.Client{Transport: rt}
   239  
   240  	return client, nil
   241  }
   242  
   243  var ThanosUserAgent = fmt.Sprintf("Thanos/%s", version.Version)
   244  
   245  type userAgentRoundTripper struct {
   246  	name string
   247  	rt   http.RoundTripper
   248  }
   249  
   250  // RoundTrip implements the http.RoundTripper interface.
   251  func (u userAgentRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
   252  	if r.UserAgent() == "" {
   253  		// The specification of http.RoundTripper says that it shouldn't mutate
   254  		// the request so make a copy of req.Header since this is all that is
   255  		// modified.
   256  		r2 := new(http.Request)
   257  		*r2 = *r
   258  		r2.Header = make(http.Header)
   259  		for k, s := range r.Header {
   260  			r2.Header[k] = s
   261  		}
   262  		r2.Header.Set("User-Agent", u.name)
   263  		r = r2
   264  	}
   265  	return u.rt.RoundTrip(r)
   266  }
   267  
   268  // EndpointsConfig configures a cluster of HTTP endpoints from static addresses and
   269  // file service discovery.
   270  type EndpointsConfig struct {
   271  	// List of addresses with DNS prefixes.
   272  	StaticAddresses []string `yaml:"static_configs"`
   273  	// List of file  configurations (our FileSD supports different DNS lookups).
   274  	FileSDConfigs []FileSDConfig `yaml:"file_sd_configs"`
   275  
   276  	// The URL scheme to use when talking to targets.
   277  	Scheme string `yaml:"scheme"`
   278  
   279  	// Path prefix to add in front of the endpoint path.
   280  	PathPrefix string `yaml:"path_prefix"`
   281  }
   282  
   283  // FileSDConfig represents a file service discovery configuration.
   284  type FileSDConfig struct {
   285  	Files           []string       `yaml:"files"`
   286  	RefreshInterval model.Duration `yaml:"refresh_interval"`
   287  }
   288  
   289  func (c FileSDConfig) convert() (file.SDConfig, error) {
   290  	var fileSDConfig file.SDConfig
   291  	b, err := yaml.Marshal(c)
   292  	if err != nil {
   293  		return fileSDConfig, err
   294  	}
   295  	err = yaml.Unmarshal(b, &fileSDConfig)
   296  	return fileSDConfig, err
   297  }
   298  
   299  type AddressProvider interface {
   300  	Resolve(context.Context, []string) error
   301  	Addresses() []string
   302  }
   303  
   304  // Client represents a client that can send requests to a cluster of HTTP-based endpoints.
   305  type Client struct {
   306  	logger log.Logger
   307  
   308  	httpClient *http.Client
   309  	scheme     string
   310  	prefix     string
   311  
   312  	staticAddresses []string
   313  	fileSDCache     *cache.Cache
   314  	fileDiscoverers []*file.Discovery
   315  
   316  	provider AddressProvider
   317  }
   318  
   319  // NewClient returns a new Client.
   320  func NewClient(logger log.Logger, cfg EndpointsConfig, client *http.Client, provider AddressProvider) (*Client, error) {
   321  	if logger == nil {
   322  		logger = log.NewNopLogger()
   323  	}
   324  
   325  	var discoverers []*file.Discovery
   326  	for _, sdCfg := range cfg.FileSDConfigs {
   327  		fileSDCfg, err := sdCfg.convert()
   328  		if err != nil {
   329  			return nil, err
   330  		}
   331  		discoverers = append(discoverers, file.NewDiscovery(&fileSDCfg, logger))
   332  	}
   333  	return &Client{
   334  		logger:          logger,
   335  		httpClient:      client,
   336  		scheme:          cfg.Scheme,
   337  		prefix:          cfg.PathPrefix,
   338  		staticAddresses: cfg.StaticAddresses,
   339  		fileSDCache:     cache.New(),
   340  		fileDiscoverers: discoverers,
   341  		provider:        provider,
   342  	}, nil
   343  }
   344  
   345  // Do executes an HTTP request with the underlying HTTP client.
   346  func (c *Client) Do(req *http.Request) (*http.Response, error) {
   347  	return c.httpClient.Do(req)
   348  }
   349  
   350  // Endpoints returns the list of known endpoints.
   351  func (c *Client) Endpoints() []*url.URL {
   352  	var urls []*url.URL
   353  	for _, addr := range c.provider.Addresses() {
   354  		urls = append(urls,
   355  			&url.URL{
   356  				Scheme: c.scheme,
   357  				Host:   addr,
   358  				Path:   path.Join("/", c.prefix),
   359  			},
   360  		)
   361  	}
   362  	return urls
   363  }
   364  
   365  // Discover runs the service to discover endpoints until the given context is done.
   366  func (c *Client) Discover(ctx context.Context) {
   367  	var wg sync.WaitGroup
   368  	ch := make(chan []*targetgroup.Group)
   369  
   370  	for _, d := range c.fileDiscoverers {
   371  		wg.Add(1)
   372  		go func(d *file.Discovery) {
   373  			d.Run(ctx, ch)
   374  			wg.Done()
   375  		}(d)
   376  	}
   377  
   378  	func() {
   379  		for {
   380  			select {
   381  			case update := <-ch:
   382  				// Discoverers sometimes send nil updates so need to check for it to avoid panics.
   383  				if update == nil {
   384  					continue
   385  				}
   386  				c.fileSDCache.Update(update)
   387  			case <-ctx.Done():
   388  				return
   389  			}
   390  		}
   391  	}()
   392  	wg.Wait()
   393  }
   394  
   395  // Resolve refreshes and resolves the list of targets.
   396  func (c *Client) Resolve(ctx context.Context) error {
   397  	return c.provider.Resolve(ctx, append(c.fileSDCache.Addresses(), c.staticAddresses...))
   398  }