mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
The primary reason for this change is that registry.NewClient was originally imposing its own decision about service discovery request policy on every other user of the shared disco.Disco object by modifying it directly. We have been moving towards using a dependency inversion style where package main is responsible for deciding how everything should be configured based on global CLI arguments, environment variables, and the CLI configuration, and so this commit moves to using that model for the HTTP clients used by the module and provider registry client code. This also makes explicit what was previously hidden away: that all service discovery requests are made using the same HTTP client policy as for requests to module registries, even if the service being discovered is not a registry. This doesn't seem to have been the intention of the code as previously written, but was still its ultimate effect: there is only one disco.Disco object shared across all discovery callers and so changing its configuration in any way changes it for everyone. This initial rework is certainly not perfect: these components were not originally designed to work in this way and there are lots of existing test cases relying on them working the old way, and so this is a compromise to get the behavior we now need (using consistent HTTP client settings across all callers) without disrupting too much existing code. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
86 lines
3.1 KiB
Go
86 lines
3.1 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package httpclient
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
)
|
|
|
|
// NewForRegistryRequests is a variant of [New] that deals with some additional
|
|
// policy concerns related to "registry requests".
|
|
//
|
|
// Exactly what constitutes a "registry request" is actually more about
|
|
// historical technical debt than intentional design, since these concerns
|
|
// were originally supposed to be handled internally within the module and
|
|
// provider registry clients but the implementation of that unfortunately caused
|
|
// the effects to "leak out" into other parts of the system, which we now
|
|
// preserve for backward compatibility here.
|
|
//
|
|
// Therefore "registry requests" includes the following:
|
|
// - All requests from the client of our network service discovery protocol,
|
|
// even though not all discoverable services are actually "registries".
|
|
// - Requests to module registries during module installation.
|
|
// - Requests to provider registries during provider installation.
|
|
//
|
|
// The retryCount argument specifies how many times requests from the resulting
|
|
// client should be automatically retried when certain transient errors occur.
|
|
//
|
|
// The timeout argument specifies a deadline for the completion of each
|
|
// request made using the client.
|
|
func NewForRegistryRequests(ctx context.Context, retryCount int, timeout time.Duration) *retryablehttp.Client {
|
|
// We'll start with the result of New, so that what we return still
|
|
// honors our general policy for HTTP client behavior.
|
|
baseClient := New(ctx)
|
|
baseClient.Timeout = timeout
|
|
|
|
// Registry requests historically offered automatic retry on certain
|
|
// transient errors implemented using the retryablehttp library, so
|
|
// we'll now deal with that here.
|
|
retryableClient := retryablehttp.NewClient()
|
|
retryableClient.HTTPClient = baseClient
|
|
retryableClient.RetryMax = retryCount
|
|
retryableClient.RequestLogHook = registryRequestLogHook
|
|
retryableClient.ErrorHandler = registryMaxRetryErrorHandler
|
|
|
|
return retryableClient
|
|
}
|
|
|
|
func registryRequestLogHook(logger retryablehttp.Logger, req *http.Request, i int) {
|
|
if i > 0 {
|
|
logger.Printf("[INFO] Failed request to %s; retrying", req.URL.String())
|
|
}
|
|
}
|
|
|
|
func registryMaxRetryErrorHandler(resp *http.Response, err error, numTries int) (*http.Response, error) {
|
|
// Close the body per library instructions
|
|
if resp != nil {
|
|
resp.Body.Close()
|
|
}
|
|
|
|
// Additional error detail: if we have a response, use the status code;
|
|
// if we have an error, use that; otherwise nothing. We will never have
|
|
// both response and error.
|
|
var errMsg string
|
|
if resp != nil {
|
|
errMsg = fmt.Sprintf(": %s returned from %s", resp.Status, resp.Request.URL)
|
|
} else if err != nil {
|
|
errMsg = fmt.Sprintf(": %s", err)
|
|
}
|
|
|
|
// This function is always called with numTries=RetryMax+1. If we made any
|
|
// retry attempts, include that in the error message.
|
|
if numTries > 1 {
|
|
return resp, fmt.Errorf("request failed after %d attempts%s",
|
|
numTries, errMsg)
|
|
}
|
|
return resp, fmt.Errorf("request failed%s", errMsg)
|
|
}
|