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 }