// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may // not use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. See the License for the specific language governing // permissions and limitations under the License. package client import ( "context" "fmt" "io" "net" "net/http" "os" "time" "github.com/aws/aws-app-mesh-agent/agent/config" "github.com/hashicorp/go-retryablehttp" log "github.com/sirupsen/logrus" ) const ( HTTP_CLIENT_TIMEOUT = 250 * time.Millisecond HTTP_CLIENT_RETRY_MAX = 3 HTTP_CLIENT_RETRY_WAIT_MIN = 100 * time.Millisecond HTTP_CLIENT_RETRY_WAIT_MAX = 1000 * time.Millisecond ) func CreateDefaultRetryableHttpClient() *retryablehttp.Client { // Default TCP retryable httpClient httpClient := retryablehttp.NewClient() httpClient.HTTPClient.Timeout = HTTP_CLIENT_TIMEOUT httpClient.RetryMax = HTTP_CLIENT_RETRY_MAX httpClient.RetryWaitMin = HTTP_CLIENT_RETRY_WAIT_MIN httpClient.RetryWaitMax = HTTP_CLIENT_RETRY_WAIT_MAX httpClient.Logger = nil // If this is not set retryablehttp client will write DEBUG logs on GET calls httpClient.ErrorHandler = RetryErrorHandler return httpClient } func CreateRetryableHttpClientForEnvoyServer(agentConfig config.AgentConfig) (*retryablehttp.Client, error) { // create UDS or TCP retryable httpClient for envoy server. For UDS, bind the client to Envoy Admin UDS path var retryableClient *retryablehttp.Client switch agentConfig.EnvoyAdminMode { case config.UDS: if _, err := os.Stat(agentConfig.EnvoyServerAdminUdsPath); os.IsNotExist(err) { msg := fmt.Sprintf("UDS path [%s] for Retryable HttpClient does not exist: %v", agentConfig.EnvoyServerAdminUdsPath, err) log.Error(msg) return nil, fmt.Errorf(msg) } httpClient := &http.Client{ Transport: &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial(config.NETWORK_SOCKET_UNIX, agentConfig.EnvoyServerAdminUdsPath) }, }, } httpClient.Timeout = HTTP_CLIENT_TIMEOUT retryableClient = CreateDefaultRetryableHttpClient() retryableClient.HTTPClient = httpClient default: retryableClient = CreateDefaultRetryableHttpClient() } return retryableClient, nil } func CreateDefaultHttpClientForEnvoyServer(agentConfig config.AgentConfig) (*http.Client, error) { // create UDS or TCP httpClient for envoy server. For UDS, bind the client to Envoy Admin UDS path. // This is similar to the previous func CreateRetryableHttpClientForEnvoyServer but without any retry. switch agentConfig.EnvoyAdminMode { case config.UDS: if _, err := os.Stat(agentConfig.EnvoyServerAdminUdsPath); os.IsNotExist(err) { msg := fmt.Sprintf("UDS path [%s] for HttpClient does not exist: %v", agentConfig.EnvoyServerAdminUdsPath, err) log.Error(msg) return nil, fmt.Errorf(msg) } return &http.Client{ Transport: &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial(config.NETWORK_SOCKET_UNIX, agentConfig.EnvoyServerAdminUdsPath) }, }, Timeout: HTTP_CLIENT_TIMEOUT, }, nil default: return CreateDefaultHttpClient(), nil } } func CreateHttpClientForAgentServer(agentConfig config.AgentConfig) (*http.Client, error) { _, err := os.Stat(agentConfig.AgentAdminUdsPath) if err != nil { return nil, fmt.Errorf("socket path [%s] does not exist", agentConfig.AgentAdminUdsPath) } return &http.Client{ Transport: &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial(config.NETWORK_SOCKET_UNIX, agentConfig.AgentAdminUdsPath) }, }, }, nil } func CreateDefaultHttpClient() *http.Client { return &http.Client{Timeout: HTTP_CLIENT_TIMEOUT} } func CreateRetryableAgentRequest(method, requestUrl string, rawBody interface{}) (*retryablehttp.Request, error) { request, err := retryablehttp.NewRequest(method, requestUrl, rawBody) if err != nil { msg := fmt.Sprintf("unable to create new retryablehttp request: %v, requestUrl: %s", err, requestUrl) log.Error(msg) return nil, fmt.Errorf(msg) } request.Header.Add("Connection", "close") request.Header.Add("User-Agent", config.APPNET_USER_AGENT) return request, nil } func CreateStandardAgentHttpRequest(method, requestUrl string, body io.Reader) (*http.Request, error) { request, err := http.NewRequest(method, requestUrl, body) if err != nil { msg := fmt.Sprintf("unable to create new http request: %v, requestUrl: %s", err, requestUrl) log.Error(msg) return nil, fmt.Errorf(msg) } request.Header.Add("Connection", "close") request.Header.Add("User-Agent", config.APPNET_USER_AGENT) return request, nil } func RetryErrorHandler(resp *http.Response, err error, attempt int) (*http.Response, error) { // ErrorHandler is called if retries are expired, containing the last status from the http library. // If not specified, default behavior for the library is to close the body and return an error indicating how many tries were attempted. // If overriding this, be sure to close the body if needed. // https://pkg.go.dev/github.com/hashicorp/go-retryablehttp#ErrorHandler // // Only one of resp and err will be non-nil. When resp is present we return the status; // when error is present, we return that. var errorMsg string if resp != nil { resp.Body.Close() errorMsg = fmt.Sprintf("status: %v", resp.Status) } else if err != nil { errorMsg = fmt.Sprintf("error: %v", err) } return resp, fmt.Errorf("giving up after %d attempt(s): %s", attempt, errorMsg) }