Files
opentf/internal/getproviders/package_location_http_archive.go
Martin Atkins 0503163e28 tracing: Centralize our OpenTelemetry package imports
OpenTelemetry has various Go packages split across several Go modules that
often need to be carefully upgraded together. And in particular, we are
using the "semconv" package in conjunction with the OpenTelemetry SDK's
"resource" package in a way that requires that they both agree on which
version of the OpenTelemetry Semantic Conventions are being followed.

To help avoid "dependency hell" situations when upgrading, this centralizes
all of our direct calls into the OpenTelemetry SDK and tracing API into
packages under internal/tracing, by exposing a few thin wrapper functions
that other packages can use to access the same functionality indirectly.

We only use a relatively small subset of the OpenTelemetry library surface
area, so we don't need too many of these reexports and they should not
represent a significant additional maintenance burden.

For the semconv and resource interaction in particular this also factors
that out into a separate helper function with a unit test, so we should
notice quickly whenever they become misaligned. This complements the
end-to-end test previously added in opentofu/opentofu#3447 to give us
faster feedback about this particular problem, while the end-to-end test
has the broader scope of making sure there aren't any errors at all when
initializing OpenTelemetry tracing.

Finally, this also replaces the constants we previously had in package
traceaddrs with functions that return attribute.KeyValue values directly.
This matches the API style used by the OpenTelemetry semconv packages, and
makes the calls to these helpers from elsewhere in the system a little
more concise.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-10-30 13:27:10 -07:00

153 lines
5.7 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 getproviders
import (
"context"
"fmt"
"log"
"net/http"
"os"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/go-retryablehttp"
"github.com/opentofu/opentofu/internal/httpclient"
"github.com/opentofu/opentofu/internal/logging"
"github.com/opentofu/opentofu/internal/tracing"
"github.com/opentofu/opentofu/internal/tracing/traceattrs"
)
// PackageHTTPURL is a provider package location accessible via HTTP.
//
// Its value is a URL string using either the http: scheme or the https: scheme.
// The URL should respond with a .zip archive whose contents are to be extracted
// into a local package directory.
//
// This type evolved from a single specific HTTP URL location to a more evolved
// type since we had to extract the logic of making an HTTP request into a more
// configurable piece to be able to provider a more specific-per-configuration
// http client.
type PackageHTTPURL struct {
// URL indicates the fully qualified location where the package
// resides and should be used as it is to perform a request.
URL string
// ClientBuilder is the external given function that is meant to
// construct a [*retryablehttp.Client] for making the actual request
// to the [PackageHTTPURL.URL] specified above.
// This came from the need of unifying the retries and timeout configurations
// into a single place. This way, the "creator" of this struct
// can inject a client by its liking to customize the requests
// accordingly.
ClientBuilder func(ctx context.Context) *retryablehttp.Client
}
var _ PackageLocation = PackageHTTPURL{}
func (p PackageHTTPURL) String() string { return p.URL }
func (p PackageHTTPURL) InstallProviderPackage(ctx context.Context, meta PackageMeta, targetDir string, allowedHashes []Hash) (*PackageAuthenticationResult, error) {
url := meta.Location.String()
ctx, span := tracing.Tracer().Start(ctx, "Install (http)", tracing.SpanAttributes(
traceattrs.URLFull(url),
))
defer span.End()
// When we're installing from an HTTP URL we expect the URL to refer to
// a zip file. We'll fetch that into a temporary file here and then
// delegate to installFromLocalArchive below to actually extract it.
// (We're not using go-getter here because its HTTP getter has a bunch
// of extraneous functionality we don't need or want, like indirection
// through X-Terraform-Get header, attempting partial fetches for
// files that already exist, etc.)
retryableClient := p.ClientBuilder(ctx)
req, err := retryablehttp.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("invalid provider download request: %w", err)
}
resp, err := retryableClient.Do(req)
if err != nil {
if ctx.Err() == context.Canceled {
// "context canceled" is not a user-friendly error message,
// so we'll return a more appropriate one here.
return nil, fmt.Errorf("provider download was interrupted")
}
return nil, fmt.Errorf("%s: %w", HostFromRequest(req.Request), err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unsuccessful request to %s: %s", url, resp.Status)
}
f, err := os.CreateTemp("", "terraform-provider")
if err != nil {
return nil, fmt.Errorf("failed to open temporary file to download from %s: %w", url, err)
}
defer os.Remove(f.Name())
defer f.Close()
// We'll borrow go-getter's "cancelable copy" implementation here so that
// the download can potentially be interrupted partway through.
n, err := getter.Copy(ctx, f, resp.Body)
if err == nil && n < resp.ContentLength {
err = fmt.Errorf("incorrect response size: expected %d bytes, but got %d bytes", resp.ContentLength, n)
}
if err != nil {
return nil, err
}
archiveFilename := f.Name()
localLocation := PackageLocalArchive(archiveFilename)
var authResult *PackageAuthenticationResult
if meta.Authentication != nil {
if authResult, err = meta.Authentication.AuthenticatePackage(localLocation); err != nil {
return authResult, err
}
}
// We can now delegate to localLocation for extraction. To do so,
// we construct a new package meta description using the local archive
// path as the location, and skipping authentication. installFromLocalMeta
// is responsible for verifying that the archive matches the allowedHashes,
// though.
localMeta := PackageMeta{
Provider: meta.Provider,
Version: meta.Version,
ProtocolVersions: meta.ProtocolVersions,
TargetPlatform: meta.TargetPlatform,
Filename: meta.Filename,
Location: localLocation,
Authentication: nil,
}
if _, err := localLocation.InstallProviderPackage(ctx, localMeta, targetDir, allowedHashes); err != nil {
return nil, err
}
return authResult, nil
}
// packageHTTPUrlClientWithRetry is the extracted logic from the [PackageHTTPURL.InstallProviderPackage] to be
// able to reuse the same logic with a custom retry.
// This is kept as it was previously, before being moved here, to avoid introducing unwanted behaviors
// in a package download process.
// Later, this method might be removed in favor of a more common client from [httpclient] package.
func packageHTTPUrlClientWithRetry(ctx context.Context, retries int) *retryablehttp.Client {
retryableClient := retryablehttp.NewClient()
retryableClient.HTTPClient = httpclient.New(ctx)
retryableClient.RetryMax = retries
retryableClient.RequestLogHook = func(logger retryablehttp.Logger, _ *http.Request, i int) {
if i > 0 {
logger.Printf("[INFO] failed to fetch provider package; retrying")
}
}
retryableClient.Logger = log.New(logging.LogOutput(), "", log.Flags())
return retryableClient
}