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>
This commit is contained in:
Martin Atkins
2025-05-09 11:39:48 -07:00
parent 3334ed5e1c
commit 65a0f7a656
19 changed files with 282 additions and 400 deletions

View File

@@ -16,6 +16,7 @@ import (
"testing"
"time"
"github.com/hashicorp/go-retryablehttp"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-svchost/disco"
@@ -26,75 +27,6 @@ import (
tfversion "github.com/opentofu/opentofu/version"
)
func TestConfigureDiscoveryRetry(t *testing.T) {
t.Run("default retry", func(t *testing.T) {
if discoveryRetry != defaultRetry {
t.Fatalf("expected retry %q, got %q", defaultRetry, discoveryRetry)
}
rc := NewClient(t.Context(), nil, nil)
if rc.client.RetryMax != defaultRetry {
t.Fatalf("expected client retry %q, got %q",
defaultRetry, rc.client.RetryMax)
}
})
t.Run("configured retry", func(t *testing.T) {
defer func() {
discoveryRetry = defaultRetry
}()
t.Setenv(registryDiscoveryRetryEnvName, "2")
configureDiscoveryRetry()
expected := 2
if discoveryRetry != expected {
t.Fatalf("expected retry %q, got %q",
expected, discoveryRetry)
}
rc := NewClient(t.Context(), nil, nil)
if rc.client.RetryMax != expected {
t.Fatalf("expected client retry %q, got %q",
expected, rc.client.RetryMax)
}
})
}
func TestConfigureRegistryClientTimeout(t *testing.T) {
t.Run("default timeout", func(t *testing.T) {
if requestTimeout != defaultRequestTimeout {
t.Fatalf("expected timeout %q, got %q",
defaultRequestTimeout.String(), requestTimeout.String())
}
rc := NewClient(t.Context(), nil, nil)
if rc.client.HTTPClient.Timeout != defaultRequestTimeout {
t.Fatalf("expected client timeout %q, got %q",
defaultRequestTimeout.String(), rc.client.HTTPClient.Timeout.String())
}
})
t.Run("configured timeout", func(t *testing.T) {
defer func() {
requestTimeout = defaultRequestTimeout
}()
t.Setenv(registryClientTimeoutEnvName, "20")
configureRequestTimeout()
expected := 20 * time.Second
if requestTimeout != expected {
t.Fatalf("expected timeout %q, got %q",
expected, requestTimeout.String())
}
rc := NewClient(t.Context(), nil, nil)
if rc.client.HTTPClient.Timeout != expected {
t.Fatalf("expected client timeout %q, got %q",
expected, rc.client.HTTPClient.Timeout.String())
}
})
}
func TestLookupModuleVersions(t *testing.T) {
server := test.Registry()
defer server.Close()
@@ -316,20 +248,20 @@ func TestLookupModuleRetryError(t *testing.T) {
}
// verify maxRetryErrorHandler handler returned the error
if !strings.Contains(err.Error(), "the request failed after 2 attempts, please try again later") {
if !strings.Contains(err.Error(), "request failed after 2 attempts") {
t.Fatal("unexpected error, got:", err)
}
}
func TestLookupModuleNoRetryError(t *testing.T) {
// Disable retries
discoveryRetry = 0
defer configureDiscoveryRetry()
server := test.RegistryRetryableErrorsServer()
defer server.Close()
client := NewClient(t.Context(), test.Disco(server), nil)
client := NewClient(
t.Context(), test.Disco(server),
// Retries are disabled by the second argument to this function
httpclient.NewForRegistryRequests(t.Context(), 0, 10*time.Second),
)
src := "example.com/test-versions/name/provider"
modsrc, err := regsrc.ParseModuleSource(src)
@@ -345,7 +277,7 @@ func TestLookupModuleNoRetryError(t *testing.T) {
}
// verify maxRetryErrorHandler handler returned the error
if !strings.Contains(err.Error(), "the request failed, please try again later") {
if !strings.Contains(err.Error(), "request failed:") {
t.Fatal("unexpected error, got:", err)
}
}
@@ -371,7 +303,7 @@ func TestLookupModuleNetworkError(t *testing.T) {
}
// verify maxRetryErrorHandler handler returned the correct error
if !strings.Contains(err.Error(), "the request failed after 2 attempts, please try again later") {
if !strings.Contains(err.Error(), "request failed after 2 attempts") {
t.Fatal("unexpected error, got:", err)
}
}
@@ -481,9 +413,9 @@ func TestModuleLocation_readRegistryResponse(t *testing.T) {
transport := &testTransport{
mockURL: mockServer.URL,
}
client := NewClient(t.Context(), test.Disco(registryServer), &http.Client{
Transport: transport,
})
httpClient := retryablehttp.NewClient()
httpClient.HTTPClient.Transport = transport
client := NewClient(t.Context(), test.Disco(registryServer), httpClient)
mod, err := regsrc.ParseModuleSource(tc.src)
if err != nil {