mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
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>
139 lines
5.7 KiB
Go
139 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 getmodules
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
|
|
getter "github.com/hashicorp/go-getter"
|
|
|
|
"github.com/opentofu/opentofu/internal/httpclient"
|
|
"github.com/opentofu/opentofu/internal/tracing"
|
|
"github.com/opentofu/opentofu/internal/tracing/traceattrs"
|
|
)
|
|
|
|
// PackageFetcher is a low-level utility for fetching remote module packages
|
|
// into local filesystem directories in preparation for use by higher-level
|
|
// module installer functionality implemented elsewhere.
|
|
//
|
|
// A PackageFetcher works only with entire module packages and never with
|
|
// the individual modules within a package.
|
|
//
|
|
// A particular PackageFetcher instance remembers the target directory of
|
|
// any successfully-installed package so that it can optimize future calls
|
|
// that have the same package address by copying the local directory tree,
|
|
// rather than fetching the package from its origin repeatedly. There is
|
|
// no way to reset this cache, so a particular PackageFetcher instance should
|
|
// live only for the duration of a single initialization process.
|
|
type PackageFetcher struct {
|
|
getter *reusingGetter
|
|
}
|
|
|
|
// NewPackageFetcher constructs a new [PackageFetcher] that interacts with
|
|
// the rest of the system and with the execution environment using the
|
|
// given [PackageFetcherEnvironment] implementation.
|
|
//
|
|
// 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(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),
|
|
}
|
|
}
|
|
|
|
// FetchPackage downloads or otherwise retrieves the filesystem inside the
|
|
// package at the given address into the given local installation directory.
|
|
//
|
|
// packageAddr must be formatted as if it were the result of an
|
|
// addrs.ModulePackage.String() call. It's only defined as a raw string here
|
|
// because the getmodules package can't import the addrs package due to
|
|
// that creating a package dependency cycle.
|
|
//
|
|
// PackageFetcher only works with entire packages. If the caller is processing
|
|
// a module source address which includes a subdirectory portion then the
|
|
// caller must resolve that itself, possibly with the help of the
|
|
// getmodules.SplitPackageSubdir and getmodules.ExpandSubdirGlobs functions.
|
|
func (f *PackageFetcher) FetchPackage(ctx context.Context, instDir string, packageAddr string) error {
|
|
ctx, span := tracing.Tracer().Start(ctx, "Fetch Package",
|
|
tracing.SpanAttributes(traceattrs.URLFull(packageAddr)),
|
|
)
|
|
defer span.End()
|
|
err := f.getter.getWithGoGetter(ctx, instDir, packageAddr)
|
|
if err != nil {
|
|
span.RecordError(err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PackageFetcherEnvironment is an interface used with [NewPackageFetcher]
|
|
// to allow the caller to define how the package fetcher should interact
|
|
// with the rest of OpenTofu and with OpenTofu's execution environment.
|
|
//
|
|
// This interface may grow to include new methods if we learn of new
|
|
// requirements, so implementations of this interface should all be
|
|
// inside this codebase. If we decide to factor out module installation
|
|
// to a separate library for broader use in future then we should consider
|
|
// carefully whether a single interface aggregating all of the fetcher's
|
|
// concerns is still the best design for that different context.
|
|
type PackageFetcherEnvironment interface {
|
|
OCIRepositoryStore(ctx context.Context, registryDomainName, repositoryPath string) (OCIRepositoryStore, error)
|
|
}
|
|
|
|
// preparePackageFetcherEnvironment takes a [PackageFetcherEnvironment]
|
|
// value provided directly by a caller to [NewPackageFetcher] and
|
|
// optionally wraps or replaces it as needed to better suit the
|
|
// implementation details of this package.
|
|
//
|
|
// Currently its only special behavior is that it replaces a nil
|
|
// PackageFetcherEnvironment with a default implementation that stubs
|
|
// out all of the methods, so that the other code in this package can
|
|
// then assume that the environment is always valid to call.
|
|
func preparePackageFetcherEnvironment(given PackageFetcherEnvironment) PackageFetcherEnvironment {
|
|
if given == nil {
|
|
return noopPackageFetcherEnvironment{}
|
|
}
|
|
return given
|
|
}
|
|
|
|
type noopPackageFetcherEnvironment struct{}
|
|
|
|
// OCIRepositoryStore implements PackageFetcherEnvironment.
|
|
func (n noopPackageFetcherEnvironment) OCIRepositoryStore(ctx context.Context, registryDomainName string, repositoryPath string) (OCIRepositoryStore, error) {
|
|
return nil, fmt.Errorf("module installation from OCI repositories is not available in this context")
|
|
}
|