mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
154 lines
5.7 KiB
Go
154 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"
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.30.0"
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
"github.com/opentofu/opentofu/internal/tracing"
|
|
)
|
|
|
|
// 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)", trace.WithAttributes(
|
|
semconv.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
|
|
}
|