package config
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/grafana/agent/pkg/config/instrumentation"
"github.com/prometheus/common/config"
)
const (
httpScheme = "http"
httpsScheme = "https"
)
type remoteOpts struct {
url *url.URL
HTTPClientConfig *config.HTTPClientConfig
}
type remoteProvider interface {
retrieve() ([]byte, error)
}
func newRemoteProvider(rawURL string, opts *remoteOpts) (remoteProvider, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("error parsing rawURL %s: %w", rawURL, err)
}
if opts == nil {
opts = &remoteOpts{}
}
opts.url = u
switch u.Scheme {
case "":
return nil, nil
case httpScheme, httpsScheme:
httpP, err := newHTTPProvider(opts)
if err != nil {
return nil, fmt.Errorf("error constructing httpProvider: %w", err)
}
return httpP, nil
default:
return nil, fmt.Errorf("remote config scheme not supported: %s", u.Scheme)
}
}
type httpProvider struct {
myURL *url.URL
httpClient *http.Client
}
func newHTTPProvider(opts *remoteOpts) (*httpProvider, error) {
httpClientConfig := config.HTTPClientConfig{}
if opts.HTTPClientConfig != nil {
err := opts.HTTPClientConfig.Validate()
if err != nil {
return nil, err
}
httpClientConfig = *opts.HTTPClientConfig
}
httpClient, err := config.NewClientFromConfig(httpClientConfig, "remote-config")
if err != nil {
return nil, err
}
return &httpProvider{
myURL: opts.url,
httpClient: httpClient,
}, nil
}
type retryAfterError struct {
retryAfter time.Duration
}
func (r retryAfterError) Error() string {
return fmt.Sprintf("server indicated to retry after %s", r.retryAfter)
}
func (p httpProvider) retrieve() ([]byte, error) {
response, err := p.httpClient.Get(p.myURL.String())
if err != nil {
instrumentation.InstrumentRemoteConfigFetchError()
return nil, fmt.Errorf("request failed: %w", err)
}
defer response.Body.Close()
instrumentation.InstrumentRemoteConfigFetch(response.StatusCode)
if response.StatusCode == http.StatusTooManyRequests {
retryAfter := response.Header.Get("Retry-After")
if retryAfter == "" {
return nil, fmt.Errorf("server indicated to retry, but no Retry-After header was provided")
}
retryAfterDuration, err := time.ParseDuration(retryAfter)
if err != nil {
return nil, fmt.Errorf("server indicated to retry, but Retry-After header was not a valid duration: %w", err)
}
return nil, retryAfterError{retryAfter: retryAfterDuration}
}
if response.StatusCode/100 != 2 {
return nil, fmt.Errorf("error fetching config: status code: %d", response.StatusCode)
}
bb, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
return bb, nil
}