httpclient: Add OTel tracing automatically when needed (#2772)

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
Martin Atkins
2025-05-09 02:16:38 -07:00
committed by GitHub
parent 11694a6ac0
commit 47875921a1
30 changed files with 154 additions and 134 deletions

View File

@@ -181,7 +181,7 @@ func realMain() int {
}
services.SetUserAgent(httpclient.OpenTofuUserAgent(version.String()))
modulePkgFetcher := remoteModulePackageFetcher(config.OCICredentialsPolicy)
modulePkgFetcher := remoteModulePackageFetcher(ctx, config.OCICredentialsPolicy)
providerSrc, diags := providerSource(config.ProviderInstallation, services, config.OCICredentialsPolicy)
if len(diags) > 0 {

View File

@@ -12,10 +12,10 @@ import (
"github.com/opentofu/opentofu/internal/getmodules"
)
func remoteModulePackageFetcher(getOCICredsPolicy ociCredsPolicyBuilder) *getmodules.PackageFetcher {
func remoteModulePackageFetcher(ctx context.Context, getOCICredsPolicy ociCredsPolicyBuilder) *getmodules.PackageFetcher {
// TODO: Pass in a real getmodules.PackageFetcherEnvironment here,
// which knows how to make use of the OCI authentication policy.
return getmodules.NewPackageFetcher(&modulePackageFetcherEnvironment{
return getmodules.NewPackageFetcher(ctx, &modulePackageFetcherEnvironment{
getOCICredsPolicy: getOCICredsPolicy,
})
}

View File

@@ -130,7 +130,7 @@ func getOCIRepositoryORASClient(ctx context.Context, registryDomain, repositoryN
return nil, fmt.Errorf("finding credentials for %q: %w", registryDomain, err)
}
return &orasAuth.Client{
Client: httpclient.New(), // the underlying HTTP client to use, preconfigured with OpenTofu's User-Agent string
Client: httpclient.New(ctx), // the underlying HTTP client to use, preconfigured with OpenTofu's User-Agent string
Credential: func(ctx context.Context, hostport string) (orasAuth.Credential, error) {
if hostport != registryDomain {
// We should not send the credentials we selected to any registry

View File

@@ -158,7 +158,7 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config
// Test modules usually do not refer to remote sources, and for local
// sources only this ultimately just records all of the module paths
// in a JSON file so that we can load them below.
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(t.Context(), nil, nil), nil)
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{}, configs.RootModuleCallForTesting())
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())

View File

@@ -135,7 +135,7 @@ func TestGet_cancel(t *testing.T) {
// This test needs a real module package fetcher instance because
// we want to attempt installing a module package from our server.
ModulePackageFetcher: getmodules.NewPackageFetcher(nil),
ModulePackageFetcher: getmodules.NewPackageFetcher(t.Context(), nil),
},
}

View File

@@ -101,7 +101,7 @@ func TestInit_fromModule_cwdDest(t *testing.T) {
// treating an absolute filesystem path as if it were a "remote"
// source address, and so we need a real package fetcher but the
// way we use it here does not cause it to make network requests.
ModulePackageFetcher: getmodules.NewPackageFetcher(nil),
ModulePackageFetcher: getmodules.NewPackageFetcher(t.Context(), nil),
},
}
@@ -141,7 +141,7 @@ func TestInit_fromModule_dstInSrc(t *testing.T) {
// treating an absolute filesystem path as if it were a "remote"
// source address, and so we need a real package fetcher but the
// way we use it here does not cause it to make network requests.
ModulePackageFetcher: getmodules.NewPackageFetcher(nil),
ModulePackageFetcher: getmodules.NewPackageFetcher(t.Context(), nil),
},
}
@@ -1866,7 +1866,7 @@ func TestInit_cancelModules(t *testing.T) {
// actually making a request to this, but we still need to provide
// the fetcher so that it will _attempt_ to make a network request
// that can then fail with a cancellation error.
ModulePackageFetcher: getmodules.NewPackageFetcher(nil),
ModulePackageFetcher: getmodules.NewPackageFetcher(t.Context(), nil),
}
c := &InitCommand{
Meta: m,

View File

@@ -50,6 +50,8 @@ type LoginCommand struct {
// Run implements cli.Command.
func (c *LoginCommand) Run(args []string) int {
ctx := c.CommandContext()
args = c.Meta.process(args)
cmdFlags := c.Meta.extendedFlagSet("login")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
@@ -192,12 +194,12 @@ func (c *LoginCommand) Run(args []string) int {
switch {
case clientConfig.SupportedGrantTypes.Has(disco.OAuthAuthzCodeGrant):
// We prefer an OAuth code grant if the server supports it.
oauthToken, tokenDiags = c.interactiveGetTokenByCode(hostname, credsCtx, clientConfig)
oauthToken, tokenDiags = c.interactiveGetTokenByCode(ctx, hostname, credsCtx, clientConfig)
case clientConfig.SupportedGrantTypes.Has(disco.OAuthOwnerPasswordGrant) && hostname == svchost.Hostname(tfeHost):
// The password grant type is allowed only for Terraform Cloud SaaS.
// Note this case is purely theoretical at this point, as TFC currently uses
// its own bespoke login protocol (tfe)
oauthToken, tokenDiags = c.interactiveGetTokenByPassword(hostname, credsCtx, clientConfig)
oauthToken, tokenDiags = c.interactiveGetTokenByPassword(ctx, hostname, credsCtx, clientConfig)
default:
tokenDiags = tokenDiags.Append(tfdiags.Sourceless(
tfdiags.Error,
@@ -209,7 +211,7 @@ func (c *LoginCommand) Run(args []string) int {
token = svcauth.HostCredentialsToken(oauthToken.AccessToken)
}
} else if tfeservice != nil {
token, tokenDiags = c.interactiveGetTokenByUI(hostname, credsCtx, tfeservice)
token, tokenDiags = c.interactiveGetTokenByUI(ctx, hostname, credsCtx, tfeservice)
}
diags = diags.Append(tokenDiags)
@@ -260,7 +262,7 @@ func (c *LoginCommand) Run(args []string) int {
req.Header.Set("Authorization", "Bearer "+token.Token())
resp, err := httpclient.New().Do(req)
resp, err := httpclient.New(ctx).Do(req)
if err != nil {
c.logMOTDError(err)
c.outputDefaultTFCLoginSuccess()
@@ -367,9 +369,9 @@ func (c *LoginCommand) defaultOutputFile() string {
return filepath.Join(c.CLIConfigDir, "credentials.tfrc.json")
}
func (c *LoginCommand) interactiveGetTokenByCode(hostname svchost.Hostname, credsCtx *loginCredentialsContext, clientConfig *disco.OAuthClient) (*oauth2.Token, tfdiags.Diagnostics) {
func (c *LoginCommand) interactiveGetTokenByCode(ctx context.Context, hostname svchost.Hostname, credsCtx *loginCredentialsContext, clientConfig *disco.OAuthClient) (*oauth2.Token, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
confirm, confirmDiags := c.interactiveContextConsent(hostname, disco.OAuthAuthzCodeGrant, credsCtx)
confirm, confirmDiags := c.interactiveContextConsent(ctx, hostname, disco.OAuthAuthzCodeGrant, credsCtx)
diags = diags.Append(confirmDiags)
if !confirm {
diags = diags.Append(errors.New("Login cancelled"))
@@ -538,7 +540,7 @@ func (c *LoginCommand) interactiveGetTokenByCode(hostname svchost.Hostname, cred
return nil, diags
}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpclient.New())
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpclient.New(ctx))
token, err := oauthConfig.Exchange(
ctx, code,
oauth2.SetAuthURLParam("code_verifier", proofKey),
@@ -555,10 +557,10 @@ func (c *LoginCommand) interactiveGetTokenByCode(hostname svchost.Hostname, cred
return token, diags
}
func (c *LoginCommand) interactiveGetTokenByPassword(hostname svchost.Hostname, credsCtx *loginCredentialsContext, clientConfig *disco.OAuthClient) (*oauth2.Token, tfdiags.Diagnostics) {
func (c *LoginCommand) interactiveGetTokenByPassword(ctx context.Context, hostname svchost.Hostname, credsCtx *loginCredentialsContext, clientConfig *disco.OAuthClient) (*oauth2.Token, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
confirm, confirmDiags := c.interactiveContextConsent(hostname, disco.OAuthOwnerPasswordGrant, credsCtx)
confirm, confirmDiags := c.interactiveContextConsent(ctx, hostname, disco.OAuthOwnerPasswordGrant, credsCtx)
diags = diags.Append(confirmDiags)
if !confirm {
diags = diags.Append(errors.New("Login cancelled"))
@@ -568,7 +570,7 @@ func (c *LoginCommand) interactiveGetTokenByPassword(hostname svchost.Hostname,
c.Ui.Output("\n---------------------------------------------------------------------------------\n")
c.Ui.Output("OpenTofu must temporarily use your password to request an API token.\nThis password will NOT be saved locally.\n")
username, err := c.UIInput().Input(context.Background(), &tofu.InputOpts{
username, err := c.UIInput().Input(ctx, &tofu.InputOpts{
Id: "username",
Query: fmt.Sprintf("Username for %s:", hostname.ForDisplay()),
})
@@ -576,7 +578,7 @@ func (c *LoginCommand) interactiveGetTokenByPassword(hostname svchost.Hostname,
diags = diags.Append(fmt.Errorf("Failed to request username: %w", err))
return nil, diags
}
password, err := c.UIInput().Input(context.Background(), &tofu.InputOpts{
password, err := c.UIInput().Input(ctx, &tofu.InputOpts{
Id: "password",
Query: fmt.Sprintf("Password for %s:", hostname.ForDisplay()),
Secret: true,
@@ -591,7 +593,7 @@ func (c *LoginCommand) interactiveGetTokenByPassword(hostname svchost.Hostname,
Endpoint: clientConfig.Endpoint(),
Scopes: clientConfig.Scopes,
}
token, err := oauthConfig.PasswordCredentialsToken(context.Background(), username, password)
token, err := oauthConfig.PasswordCredentialsToken(ctx, username, password)
if err != nil {
// FIXME: The OAuth2 library generates errors that are not appropriate
// for a Terraform end-user audience, so once we have more experience
@@ -607,10 +609,10 @@ func (c *LoginCommand) interactiveGetTokenByPassword(hostname svchost.Hostname,
return token, diags
}
func (c *LoginCommand) interactiveGetTokenByUI(hostname svchost.Hostname, credsCtx *loginCredentialsContext, service *url.URL) (svcauth.HostCredentialsToken, tfdiags.Diagnostics) {
func (c *LoginCommand) interactiveGetTokenByUI(ctx context.Context, hostname svchost.Hostname, credsCtx *loginCredentialsContext, service *url.URL) (svcauth.HostCredentialsToken, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
confirm, confirmDiags := c.interactiveContextConsent(hostname, disco.OAuthGrantType(""), credsCtx)
confirm, confirmDiags := c.interactiveContextConsent(ctx, hostname, disco.OAuthGrantType(""), credsCtx)
diags = diags.Append(confirmDiags)
if !confirm {
diags = diags.Append(errors.New("Login cancelled"))
@@ -659,7 +661,7 @@ func (c *LoginCommand) interactiveGetTokenByUI(hostname svchost.Hostname, credsC
}
}
token, err := c.UIInput().Input(context.Background(), &tofu.InputOpts{
token, err := c.UIInput().Input(ctx, &tofu.InputOpts{
Id: "token",
Query: fmt.Sprintf("Token for %s:", hostname.ForDisplay()),
Secret: true,
@@ -685,7 +687,7 @@ func (c *LoginCommand) interactiveGetTokenByUI(hostname svchost.Hostname, credsC
diags = diags.Append(fmt.Errorf("Failed to create API client: %w", err))
return "", diags
}
user, err := client.Users.ReadCurrent(context.Background())
user, err := client.Users.ReadCurrent(ctx)
if err == tfe.ErrUnauthorized {
diags = diags.Append(fmt.Errorf("Token is invalid: %w", err))
return "", diags
@@ -698,7 +700,7 @@ func (c *LoginCommand) interactiveGetTokenByUI(hostname svchost.Hostname, credsC
return svcauth.HostCredentialsToken(token), nil
}
func (c *LoginCommand) interactiveContextConsent(hostname svchost.Hostname, grantType disco.OAuthGrantType, credsCtx *loginCredentialsContext) (bool, tfdiags.Diagnostics) {
func (c *LoginCommand) interactiveContextConsent(ctx context.Context, hostname svchost.Hostname, grantType disco.OAuthGrantType, credsCtx *loginCredentialsContext) (bool, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
mechanism := "OAuth"
if grantType == "" {
@@ -724,7 +726,7 @@ func (c *LoginCommand) interactiveContextConsent(hostname svchost.Hostname, gran
}
}
v, err := c.UIInput().Input(context.Background(), &tofu.InputOpts{
v, err := c.UIInput().Input(ctx, &tofu.InputOpts{
Id: "approve",
Query: "Do you want to proceed?",
Description: `Only 'yes' will be accepted to confirm.`,

View File

@@ -287,7 +287,7 @@ func (m *Meta) installModules(ctx context.Context, rootDir, testsDir string, upg
return true, diags
}
inst := initwd.NewModuleInstaller(m.modulesDir(), loader, m.registryClient(), m.ModulePackageFetcher)
inst := initwd.NewModuleInstaller(m.modulesDir(), loader, m.registryClient(ctx), m.ModulePackageFetcher)
call, vDiags := m.rootModuleCall(ctx, rootDir)
diags = diags.Append(vDiags)
@@ -324,7 +324,7 @@ func (m *Meta) initDirFromModule(ctx context.Context, targetDir string, addr str
}
targetDir = m.normalizePath(targetDir)
moreDiags := initwd.DirFromModule(ctx, loader, targetDir, m.modulesDir(), addr, m.registryClient(), m.ModulePackageFetcher, hooks)
moreDiags := initwd.DirFromModule(ctx, loader, targetDir, m.modulesDir(), addr, m.registryClient(ctx), m.ModulePackageFetcher, hooks)
diags = diags.Append(moreDiags)
if ctx.Err() == context.Canceled {
m.showDiagnostics(diags)
@@ -441,7 +441,6 @@ func (m *Meta) initConfigLoader() (*configload.Loader, error) {
if m.configLoader == nil {
loader, err := configload.NewLoader(&configload.Config{
ModulesDir: m.modulesDir(),
Services: m.Services,
})
if err != nil {
return nil, err
@@ -456,8 +455,8 @@ func (m *Meta) initConfigLoader() (*configload.Loader, error) {
}
// registryClient instantiates and returns a new Registry client.
func (m *Meta) registryClient() *registry.Client {
return registry.NewClient(m.Services, nil)
func (m *Meta) registryClient(ctx context.Context) *registry.Client {
return registry.NewClient(ctx, m.Services, nil)
}
// configValueFromCLI parses a configuration value that was provided in a

View File

@@ -119,7 +119,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int {
// generality of go-getter but it's still handy to use the HTTP getter
// as an easy way to download over HTTP into a file on disk.
httpGetter := getter.HttpGetter{
Client: httpclient.New(),
Client: httpclient.New(ctx),
Netrc: true,
XTerraformGetDisabled: true,
}

View File

@@ -16,7 +16,7 @@ import (
//
// See the documentation of MockLauncher itself for more information.
func NewMockLauncher(ctx context.Context) *MockLauncher {
client := httpclient.New()
client := httpclient.New(ctx)
return &MockLauncher{
Client: client,
Context: ctx,

View File

@@ -10,10 +10,9 @@ import (
"path/filepath"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform-svchost/disco"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/registry"
"github.com/spf13/afero"
"github.com/opentofu/opentofu/internal/configs"
)
// A Loader instance is the main entry-point for loading configurations via
@@ -39,12 +38,6 @@ type Config struct {
// .terraform/modules directory, in the common case where this package
// is being loaded from the main OpenTofu CLI package.)
ModulesDir string
// Services is the service discovery client to use when locating remote
// module registry endpoints. If this is nil then registry sources are
// not supported, which should be true only in specialized circumstances
// such as in tests.
Services *disco.Disco
}
// NewLoader creates and returns a loader that reads configuration from the
@@ -56,7 +49,6 @@ type Config struct {
func NewLoader(config *Config) (*Loader, error) {
fs := afero.NewOsFs()
parser := configs.NewParser(fs)
reg := registry.NewClient(config.Services, nil)
ret := &Loader{
parser: parser,
@@ -64,8 +56,6 @@ func NewLoader(config *Config) (*Loader, error) {
FS: afero.Afero{Fs: fs},
CanInstall: true,
Dir: config.ModulesDir,
Services: config.Services,
Registry: reg,
},
}

View File

@@ -9,10 +9,9 @@ import (
"os"
"path/filepath"
"github.com/hashicorp/terraform-svchost/disco"
"github.com/opentofu/opentofu/internal/modsdir"
"github.com/opentofu/opentofu/internal/registry"
"github.com/spf13/afero"
"github.com/opentofu/opentofu/internal/modsdir"
)
type moduleMgr struct {
@@ -28,15 +27,6 @@ type moduleMgr struct {
// Dir is the path where descendent modules are (or will be) installed.
Dir string
// Services is a service discovery client that will be used to find
// remote module registry endpoints. This object may be pre-loaded with
// cached discovery information.
Services *disco.Disco
// Registry is a client for the module registry protocol, which is used
// when a module is requested from a registry source.
Registry *registry.Client
// manifest tracks the currently-installed modules for this manager.
//
// The loader may read this. Only the installer may write to it, and

View File

@@ -9,14 +9,11 @@ import (
"context"
"fmt"
"log"
"net/http"
"os"
"strings"
"sync"
cleanhttp "github.com/hashicorp/go-cleanhttp"
getter "github.com/hashicorp/go-getter"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"github.com/opentofu/opentofu/internal/copy"
)
@@ -106,22 +103,12 @@ var goGetterGetters = map[string]getter.Getter{
"gcs": new(getter.GCSGetter),
"git": new(getter.GitGetter),
"hg": new(getter.HgGetter),
"http": getterHTTPGetter,
"https": getterHTTPGetter,
"http": nil, // configured dynamically in NewPackageFetcher
"https": nil, // configured dynamically in NewPackageFetcher
"oci": nil, // configured dynamically using [PackageFetcherEnvironment.OCIRepositoryStore]
"s3": new(getter.S3Getter),
}
var getterHTTPClient = &http.Client{
Transport: otelhttp.NewTransport(cleanhttp.DefaultTransport()),
}
var getterHTTPGetter = &getter.HttpGetter{
Client: getterHTTPClient,
Netrc: true,
XTerraformGetLimit: 10,
}
// A reusingGetter is a helper for the module installer that remembers
// the final resolved addresses of all of the sources it has already been
// asked to install, and will copy from a prior installation directory if

View File

@@ -13,6 +13,8 @@ import (
semconv "go.opentelemetry.io/otel/semconv/v1.30.0"
"go.opentelemetry.io/otel/trace"
getter "github.com/hashicorp/go-getter"
"github.com/opentofu/opentofu/internal/httpclient"
"github.com/opentofu/opentofu/internal/tracing"
)
@@ -40,18 +42,35 @@ type PackageFetcher struct {
// It's valid to set "env" to nil, but that will make certain module
// package source types unavailable for use and so that concession is
// intended only for use in unit tests.
func NewPackageFetcher(env PackageFetcherEnvironment) *PackageFetcher {
func NewPackageFetcher(ctx context.Context, env PackageFetcherEnvironment) *PackageFetcher {
env = preparePackageFetcherEnvironment(env)
var httpClient = httpclient.New(ctx)
// We use goGetterGetters as our starting point for the available
// getters, but some need to be instantiated dynamically based on
// the given "env". We shallow-copy the source map so that multiple
// instances of PackageFetcher don't clobber each other's getters.
getters := maps.Clone(goGetterGetters)
// The OCI Distribution getter needs to acquire credentials based on
// centrally-configured policy, encapsulated in env.OCIRepositoryStore.
getters["oci"] = &ociDistributionGetter{
getOCIRepositoryStore: env.OCIRepositoryStore,
}
// The HTTP getter (used for both "http" and "https" schemes) uses
// the HTTP client we instantiated above, whose behavior can be
// incluenced by the ctx argument we passed to it, such as by
// enabling OpenTelemetry tracing when appropriate.
var httpGetter = &getter.HttpGetter{
Client: httpClient,
Netrc: true,
XTerraformGetLimit: 10,
}
getters["http"] = httpGetter
getters["https"] = httpGetter
return &PackageFetcher{
getter: newReusingGetter(getters),
}

View File

@@ -178,7 +178,7 @@ func findLegacyProviderLookupSource(host svchost.Hostname, source Source) *Regis
// depend on it, and this fallback mechanism will likely be removed altogether
// in a future OpenTofu version.
func (s *RegistrySource) lookupLegacyProviderNamespace(ctx context.Context, hostname svchost.Hostname, typeName string) (string, string, error) {
client, err := s.registryClient(hostname)
client, err := s.registryClient(ctx, hostname)
if err != nil {
return "", "", err
}

View File

@@ -47,7 +47,7 @@ var _ Source = (*HTTPMirrorSource)(nil)
// UI/config layer's responsibility to validate this and return a suitable
// error message for the end-user audience.)
func NewHTTPMirrorSource(baseURL *url.URL, creds svcauth.CredentialsSource) *HTTPMirrorSource {
httpClient := httpclient.New()
httpClient := httpclient.New(context.TODO())
httpClient.Timeout = requestTimeout
httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
// If we get redirected more than five times we'll assume we're

View File

@@ -15,7 +15,6 @@ import (
"github.com/hashicorp/go-getter"
"github.com/hashicorp/go-retryablehttp"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
semconv "go.opentelemetry.io/otel/semconv/v1.30.0"
"go.opentelemetry.io/otel/trace"
@@ -52,8 +51,7 @@ func (p PackageHTTPURL) InstallProviderPackage(ctx context.Context, meta Package
// files that already exist, etc.)
retryableClient := retryablehttp.NewClient()
retryableClient.HTTPClient = httpclient.New()
retryableClient.HTTPClient.Transport = otelhttp.NewTransport(retryableClient.HTTPClient.Transport)
retryableClient.HTTPClient = httpclient.New(ctx)
retryableClient.RetryMax = maxHTTPPackageRetryCount
retryableClient.RequestLogHook = func(logger retryablehttp.Logger, _ *http.Request, i int) {
if i > 0 {

View File

@@ -24,7 +24,6 @@ import (
"github.com/hashicorp/go-retryablehttp"
svchost "github.com/hashicorp/terraform-svchost"
svcauth "github.com/hashicorp/terraform-svchost/auth"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
otelAttr "go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"go.opentelemetry.io/otel/trace"
@@ -78,8 +77,8 @@ type registryClient struct {
httpClient *retryablehttp.Client
}
func newRegistryClient(baseURL *url.URL, creds svcauth.HostCredentials) *registryClient {
httpClient := httpclient.New()
func newRegistryClient(ctx context.Context, baseURL *url.URL, creds svcauth.HostCredentials) *registryClient {
httpClient := httpclient.New(ctx)
httpClient.Timeout = requestTimeout
retryableClient := retryablehttp.NewClient()
@@ -88,8 +87,6 @@ func newRegistryClient(baseURL *url.URL, creds svcauth.HostCredentials) *registr
retryableClient.RequestLogHook = requestLogHook
retryableClient.ErrorHandler = maxRetryErrorHandler
retryableClient.HTTPClient.Transport = otelhttp.NewTransport(retryableClient.HTTPClient.Transport)
retryableClient.Logger = log.New(logging.LogOutput(), "", log.Flags())
return &registryClient{

View File

@@ -6,7 +6,6 @@
package getproviders
import (
"context"
"encoding/json"
"fmt"
"log"
@@ -29,7 +28,7 @@ func TestConfigureDiscoveryRetry(t *testing.T) {
t.Fatalf("expected retry %q, got %q", registryClientDefaultRetry, discoveryRetry)
}
rc := newRegistryClient(nil, nil)
rc := newRegistryClient(t.Context(), nil, nil)
if rc.httpClient.RetryMax != registryClientDefaultRetry {
t.Fatalf("expected client retry %q, got %q",
registryClientDefaultRetry, rc.httpClient.RetryMax)
@@ -49,7 +48,7 @@ func TestConfigureDiscoveryRetry(t *testing.T) {
expected, discoveryRetry)
}
rc := newRegistryClient(nil, nil)
rc := newRegistryClient(t.Context(), nil, nil)
if rc.httpClient.RetryMax != expected {
t.Fatalf("expected client retry %q, got %q",
expected, rc.httpClient.RetryMax)
@@ -64,7 +63,7 @@ func TestConfigureRegistryClientTimeout(t *testing.T) {
defaultRequestTimeout.String(), requestTimeout.String())
}
rc := newRegistryClient(nil, nil)
rc := newRegistryClient(t.Context(), nil, nil)
if rc.httpClient.HTTPClient.Timeout != defaultRequestTimeout {
t.Fatalf("expected client timeout %q, got %q",
defaultRequestTimeout.String(), rc.httpClient.HTTPClient.Timeout.String())
@@ -84,7 +83,7 @@ func TestConfigureRegistryClientTimeout(t *testing.T) {
expected, requestTimeout.String())
}
rc := newRegistryClient(nil, nil)
rc := newRegistryClient(t.Context(), nil, nil)
if rc.httpClient.HTTPClient.Timeout != expected {
t.Fatalf("expected client timeout %q, got %q",
expected, rc.httpClient.HTTPClient.Timeout.String())
@@ -342,12 +341,12 @@ func TestProviderVersions(t *testing.T) {
}
for _, test := range tests {
t.Run(test.provider.String(), func(t *testing.T) {
client, err := source.registryClient(test.provider.Hostname)
client, err := source.registryClient(t.Context(), test.provider.Hostname)
if err != nil {
t.Fatal(err)
}
gotVersions, _, err := client.ProviderVersions(context.Background(), test.provider)
gotVersions, _, err := client.ProviderVersions(t.Context(), test.provider)
if err != nil {
if test.wantErr == "" {
@@ -427,12 +426,12 @@ func TestFindClosestProtocolCompatibleVersion(t *testing.T) {
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
client, err := source.registryClient(test.provider.Hostname)
client, err := source.registryClient(t.Context(), test.provider.Hostname)
if err != nil {
t.Fatal(err)
}
got, err := client.findClosestProtocolCompatibleVersion(context.Background(), test.provider, test.version)
got, err := client.findClosestProtocolCompatibleVersion(t.Context(), test.provider, test.version)
if err != nil {
if test.wantErr == "" {

View File

@@ -39,7 +39,7 @@ func NewRegistrySource(services *disco.Disco) *RegistrySource {
// ErrProviderNotKnown, or ErrQueryFailed. Callers must be defensive and
// expect errors of other types too, to allow for future expansion.
func (s *RegistrySource) AvailableVersions(ctx context.Context, provider addrs.Provider) (VersionList, Warnings, error) {
client, err := s.registryClient(provider.Hostname)
client, err := s.registryClient(ctx, provider.Hostname)
if err != nil {
return nil, nil, err
}
@@ -101,7 +101,7 @@ func (s *RegistrySource) AvailableVersions(ctx context.Context, provider addrs.P
// ErrPlatformNotSupported, or ErrQueryFailed. Callers must be defensive and
// expect errors of other types too, to allow for future expansion.
func (s *RegistrySource) PackageMeta(ctx context.Context, provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
client, err := s.registryClient(provider.Hostname)
client, err := s.registryClient(ctx, provider.Hostname)
if err != nil {
return PackageMeta{}, err
}
@@ -109,7 +109,7 @@ func (s *RegistrySource) PackageMeta(ctx context.Context, provider addrs.Provide
return client.PackageMeta(ctx, provider, version, target)
}
func (s *RegistrySource) registryClient(hostname svchost.Hostname) (*registryClient, error) {
func (s *RegistrySource) registryClient(ctx context.Context, hostname svchost.Hostname) (*registryClient, error) {
host, err := s.services.Discover(hostname)
if err != nil {
return nil, ErrHostUnreachable{
@@ -147,7 +147,7 @@ func (s *RegistrySource) registryClient(hostname svchost.Hostname) (*registryCli
return nil, fmt.Errorf("failed to retrieve credentials for %s: %w", hostname, err)
}
return newRegistryClient(url, creds), nil
return newRegistryClient(ctx, url, creds), nil
}
func (s *RegistrySource) ForDisplay(provider addrs.Provider) string {

View File

@@ -6,19 +6,61 @@
package httpclient
import (
"context"
"net/http"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
otelTrace "go.opentelemetry.io/otel/trace"
"github.com/opentofu/opentofu/version"
)
// New returns the DefaultPooledClient from the cleanhttp
// package that will also send a OpenTofu User-Agent string.
func New() *http.Client {
//
// If the given context has an active OpenTelemetry trace span associated with
// it then the returned client is also configured to collect traces for
// outgoing requests. However, those traces will be children of the span
// associated with the context passed _in each individual request_, rather
// than of the span in the context passed to this function; this function
// only checks for the presence of any span as a heuristic for whether the
// caller is in a part of the codebase that has OpenTelemetry plumbing in
// place, and does not actually make use of any information from that span.
func New(ctx context.Context) *http.Client {
cli := cleanhttp.DefaultPooledClient()
cli.Transport = &userAgentRoundTripper{
userAgent: OpenTofuUserAgent(version.Version),
inner: cli.Transport,
}
if span := otelTrace.SpanFromContext(ctx); span != nil && span.IsRecording() {
// We consider the presence of an active span -- that is, one whose
// presence is going to be reported to a trace collector outside of
// the OpenTofu process -- as sufficient signal that generating
// spans for requests made with the returned client will be useful.
//
// The following has two important implications:
// - Any request made using the returned client will generate an
// OpenTelemetry tracing span using the standard semantic conventions
// for an outgoing HTTP request. Therefore all requests made with
// this client must also be passed a context.Context carrying a
// suitable parent span that the request will be reported as a child
// of.
// - The outgoing request will include trace context metadata using
// the conventions from following specification, which would allow
// the recieving server to contribute its own child spans to the
// trace if it has access to the same collector:
//
// https://www.w3.org/TR/trace-context/
//
// We do this only when there seems to be an active span because
// otherwise each HTTP request without an active trace context will
// cause a separate trace to begin, containing only that HTTP request,
// which would create confusing noise for whoever is consuming the
// traces.
cli.Transport = otelhttp.NewTransport(cli.Transport)
}
return cli
}

View File

@@ -75,7 +75,7 @@ func TestNew_userAgent(t *testing.T) {
} {
t.Run(fmt.Sprintf("%d %s", i, c.expected), func(t *testing.T) {
actualUserAgent = ""
cli := New()
cli := New(t.Context())
err := c.request(cli)
if err != nil {
t.Fatal(err)

View File

@@ -44,7 +44,7 @@ func TestDirFromModule_registry(t *testing.T) {
hooks := &testInstallHooks{}
reg := registry.NewClient(nil, nil)
reg := registry.NewClient(t.Context(), nil, nil)
loader := configload.NewLoaderForTests(t)
diags := DirFromModule(context.Background(), loader, dir, modsDir, "hashicorp/module-installer-acctest/aws//examples/main", reg, nil, hooks)
assertNoDiagnostics(t, diags)
@@ -173,7 +173,7 @@ func TestDirFromModule_submodules(t *testing.T) {
// treating an absolute filesystem path as if it were a "remote"
// source address, and so we need a real package fetcher but the
// way we use it here does not cause it to make network requests.
getmodules.NewPackageFetcher(nil),
getmodules.NewPackageFetcher(t.Context(), nil),
hooks,
)
assertNoDiagnostics(t, diags)
@@ -257,7 +257,7 @@ func TestDirFromModule_submodulesWithProvider(t *testing.T) {
// treating an absolute filesystem path as if it were a "remote"
// source address, and so we need a real package fetcher but the
// way we use it here does not cause it to make network requests.
getmodules.NewPackageFetcher(nil),
getmodules.NewPackageFetcher(t.Context(), nil),
hooks,
)
@@ -325,7 +325,7 @@ func TestDirFromModule_rel_submodules(t *testing.T) {
// treating an absolute filesystem path as if it were a "remote"
// source address, and so we need a real package fetcher but the
// way we use it here does not cause it to make network requests.
getmodules.NewPackageFetcher(nil),
getmodules.NewPackageFetcher(t.Context(), nil),
hooks,
)
assertNoDiagnostics(t, diags)

View File

@@ -144,7 +144,7 @@ func TestModuleInstaller_invalidModuleName(t *testing.T) {
modulesDir := filepath.Join(dir, ".terraform/modules")
loader := configload.NewLoaderForTests(t)
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil), nil)
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(t.Context(), nil, nil), nil)
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
t.Fatal("expected error")
@@ -183,7 +183,7 @@ func TestModuleInstaller_packageEscapeError(t *testing.T) {
// the esoteric legacy support for treating an absolute filesystem path
// as if it were a "remote package". This should not use any of the
// truly-"remote" module sources, even though it technically has access to.
inst := NewModuleInstaller(modulesDir, loader, nil, getmodules.NewPackageFetcher(nil))
inst := NewModuleInstaller(modulesDir, loader, nil, getmodules.NewPackageFetcher(t.Context(), nil))
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
if !diags.HasErrors() {
@@ -223,7 +223,7 @@ func TestModuleInstaller_explicitPackageBoundary(t *testing.T) {
// the esoteric legacy support for treating an absolute filesystem path
// as if it were a "remote package". This should not use any of the
// truly-"remote" module sources, even though it technically has access to.
inst := NewModuleInstaller(modulesDir, loader, nil, getmodules.NewPackageFetcher(nil))
inst := NewModuleInstaller(modulesDir, loader, nil, getmodules.NewPackageFetcher(t.Context(), nil))
_, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
if diags.HasErrors() {
@@ -307,7 +307,7 @@ func TestModuleInstaller_Prerelease(t *testing.T) {
modulesDir := filepath.Join(dir, ".terraform/modules")
loader := configload.NewLoaderForTests(t)
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil), nil)
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(t.Context(), nil, nil), nil)
cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks, configs.RootModuleCallForTesting())
if tc.shouldError {
@@ -482,7 +482,7 @@ func TestLoaderInstallModules_registry(t *testing.T) {
modulesDir := filepath.Join(dir, ".terraform/modules")
loader := configload.NewLoaderForTests(t)
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil), nil)
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(t.Context(), nil, nil), nil)
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
@@ -643,7 +643,7 @@ func TestLoaderInstallModules_goGetter(t *testing.T) {
modulesDir := filepath.Join(dir, ".terraform/modules")
loader := configload.NewLoaderForTests(t)
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil), nil)
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(t.Context(), nil, nil), nil)
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)
@@ -813,7 +813,7 @@ func TestLoadInstallModules_registryFromTest(t *testing.T) {
modulesDir := filepath.Join(dir, ".terraform/modules")
loader := configload.NewLoaderForTests(t)
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil), nil)
inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(t.Context(), nil, nil), nil)
_, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks, configs.RootModuleCallForTesting())
assertNoDiagnostics(t, diags)

View File

@@ -34,7 +34,7 @@ func LoadConfigForTests(t testing.TB, rootDir string, testsDir string) (*configs
var diags tfdiags.Diagnostics
loader := configload.NewLoaderForTests(t)
inst := NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
inst := NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(t.Context(), nil, nil), nil)
call := configs.RootModuleCallForTesting()
_, moreDiags := inst.InstallModules(t.Context(), rootDir, testsDir, true, false, ModuleInstallHooksImpl{}, call)

View File

@@ -25,7 +25,7 @@ func testAnalyzer(t *testing.T, fixtureName string) *Analyzer {
configDir := filepath.Join("testdata", fixtureName)
loader := configload.NewLoaderForTests(t)
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(t.Context(), nil, nil), nil)
_, instDiags := inst.InstallModules(context.Background(), configDir, "tests", true, false, initwd.ModuleInstallHooksImpl{}, configs.RootModuleCallForTesting())
if instDiags.HasErrors() {
t.Fatalf("unexpected module installation errors: %s", instDiags.Err().Error())

View File

@@ -539,7 +539,7 @@ func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instance
t.Helper()
loader := configload.NewLoaderForTests(t)
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(t.Context(), nil, nil), nil)
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{}, configs.RootModuleCallForTesting())
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())

View File

@@ -22,7 +22,6 @@ import (
"github.com/hashicorp/go-retryablehttp"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/hashicorp/terraform-svchost/disco"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
otelAttr "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
@@ -80,13 +79,13 @@ type Client struct {
}
// NewClient returns a new initialized registry client.
func NewClient(services *disco.Disco, client *http.Client) *Client {
func NewClient(ctx context.Context, services *disco.Disco, client *http.Client) *Client {
if services == nil {
services = disco.New()
}
if client == nil {
client = httpclient.New()
client = httpclient.New(ctx)
client.Timeout = requestTimeout
}
retryableClient := retryablehttp.NewClient()
@@ -95,8 +94,6 @@ func NewClient(services *disco.Disco, client *http.Client) *Client {
retryableClient.RequestLogHook = requestLogHook
retryableClient.ErrorHandler = maxRetryErrorHandler
retryableClient.HTTPClient.Transport = otelhttp.NewTransport(retryableClient.HTTPClient.Transport)
logOutput := logging.LogOutput()
retryableClient.Logger = log.New(logOutput, "", log.Flags())

View File

@@ -32,7 +32,7 @@ func TestConfigureDiscoveryRetry(t *testing.T) {
t.Fatalf("expected retry %q, got %q", defaultRetry, discoveryRetry)
}
rc := NewClient(nil, nil)
rc := NewClient(t.Context(), nil, nil)
if rc.client.RetryMax != defaultRetry {
t.Fatalf("expected client retry %q, got %q",
defaultRetry, rc.client.RetryMax)
@@ -52,7 +52,7 @@ func TestConfigureDiscoveryRetry(t *testing.T) {
expected, discoveryRetry)
}
rc := NewClient(nil, nil)
rc := NewClient(t.Context(), nil, nil)
if rc.client.RetryMax != expected {
t.Fatalf("expected client retry %q, got %q",
expected, rc.client.RetryMax)
@@ -67,7 +67,7 @@ func TestConfigureRegistryClientTimeout(t *testing.T) {
defaultRequestTimeout.String(), requestTimeout.String())
}
rc := NewClient(nil, nil)
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())
@@ -87,7 +87,7 @@ func TestConfigureRegistryClientTimeout(t *testing.T) {
expected, requestTimeout.String())
}
rc := NewClient(nil, nil)
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())
@@ -99,7 +99,7 @@ func TestLookupModuleVersions(t *testing.T) {
server := test.Registry()
defer server.Close()
client := NewClient(test.Disco(server), nil)
client := NewClient(t.Context(), test.Disco(server), nil)
// test with and without a hostname
for _, src := range []string{
@@ -143,7 +143,7 @@ func TestInvalidRegistry(t *testing.T) {
server := test.Registry()
defer server.Close()
client := NewClient(test.Disco(server), nil)
client := NewClient(t.Context(), test.Disco(server), nil)
src := "non-existent.localhost.localdomain/test-versions/name/provider"
modsrc, err := regsrc.ParseModuleSource(src)
@@ -160,7 +160,7 @@ func TestRegistryAuth(t *testing.T) {
server := test.Registry()
defer server.Close()
client := NewClient(test.Disco(server), nil)
client := NewClient(t.Context(), test.Disco(server), nil)
src := "private/name/provider"
mod, err := regsrc.ParseModuleSource(src)
@@ -195,7 +195,7 @@ func TestLookupModuleLocationRelative(t *testing.T) {
server := test.Registry()
defer server.Close()
client := NewClient(test.Disco(server), nil)
client := NewClient(t.Context(), test.Disco(server), nil)
src := "relative/foo/bar"
mod, err := regsrc.ParseModuleSource(src)
@@ -231,7 +231,7 @@ func TestAccLookupModuleVersions(t *testing.T) {
t.Fatal(err)
}
s := NewClient(regDisco, nil)
s := NewClient(t.Context(), regDisco, nil)
resp, err := s.ModuleVersions(context.Background(), modsrc)
if err != nil {
t.Fatal(err)
@@ -265,7 +265,7 @@ func TestLookupLookupModuleError(t *testing.T) {
server := test.Registry()
defer server.Close()
client := NewClient(test.Disco(server), nil)
client := NewClient(t.Context(), test.Disco(server), nil)
// this should not be found in the registry
src := "bad/local/path"
@@ -300,7 +300,7 @@ func TestLookupModuleRetryError(t *testing.T) {
server := test.RegistryRetryableErrorsServer()
defer server.Close()
client := NewClient(test.Disco(server), nil)
client := NewClient(t.Context(), test.Disco(server), nil)
src := "example.com/test-versions/name/provider"
modsrc, err := regsrc.ParseModuleSource(src)
@@ -329,7 +329,7 @@ func TestLookupModuleNoRetryError(t *testing.T) {
server := test.RegistryRetryableErrorsServer()
defer server.Close()
client := NewClient(test.Disco(server), nil)
client := NewClient(t.Context(), test.Disco(server), nil)
src := "example.com/test-versions/name/provider"
modsrc, err := regsrc.ParseModuleSource(src)
@@ -352,7 +352,7 @@ func TestLookupModuleNoRetryError(t *testing.T) {
func TestLookupModuleNetworkError(t *testing.T) {
server := test.RegistryRetryableErrorsServer()
client := NewClient(test.Disco(server), nil)
client := NewClient(t.Context(), test.Disco(server), nil)
// Shut down the server to simulate network failure
server.Close()
@@ -481,7 +481,7 @@ func TestModuleLocation_readRegistryResponse(t *testing.T) {
transport := &testTransport{
mockURL: mockServer.URL,
}
client := NewClient(test.Disco(registryServer), &http.Client{
client := NewClient(t.Context(), test.Disco(registryServer), &http.Client{
Transport: transport,
})

View File

@@ -65,7 +65,7 @@ func testModuleWithSnapshot(t testing.TB, name string) (*configs.Config, *config
// Test modules usually do not refer to remote sources, and for local
// sources only this ultimately just records all of the module paths
// in a JSON file so that we can load them below.
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(t.Context(), nil, nil), nil)
_, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{}, configs.RootModuleCallForTesting())
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
@@ -121,7 +121,7 @@ func testModuleInline(t testing.TB, sources map[string]string) *configs.Config {
// Test modules usually do not refer to remote sources, and for local
// sources only this ultimately just records all of the module paths
// in a JSON file so that we can load them below.
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil), nil)
inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(t.Context(), nil, nil), nil)
_, instDiags := inst.InstallModules(context.Background(), cfgPath, "tests", true, false, initwd.ModuleInstallHooksImpl{}, configs.RootModuleCallForTesting())
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())