Files
opentf/internal/httpclient/registry_client.go
Martin Atkins 65a0f7a656 registry+getproviders: Registry client policy centralized in main
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>
2025-05-12 10:50:17 -07:00

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)
}