mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
main+getmodules+getproviders: OTel tracing for OCI repo installation
This adds more detailed OTel trace spans to our various different interactions with OCI repositories, which is helpful to understand the time spent in each of the various sequential steps involved in resolving an OCI artifact. OTel's centrally-maintained conventions for attribute names currently only have a standard for reporting a manifest digest, so we'll use that where it's appropriate but use our own "opentofu.oci."-prefixed attribute names for everything else for now. Hopefully the upstream standard will be broadened later to include some additional concepts, at which point we can switch over to the standardized attribute names. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
@@ -15,6 +15,8 @@ import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
otelAttr "go.opentelemetry.io/otel/attribute"
|
||||
otelTrace "go.opentelemetry.io/otel/trace"
|
||||
orasRemote "oras.land/oras-go/v2/registry/remote"
|
||||
orasAuth "oras.land/oras-go/v2/registry/remote/auth"
|
||||
orasCreds "oras.land/oras-go/v2/registry/remote/credentials"
|
||||
@@ -24,6 +26,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/getmodules"
|
||||
"github.com/opentofu/opentofu/internal/getproviders"
|
||||
"github.com/opentofu/opentofu/internal/httpclient"
|
||||
"github.com/opentofu/opentofu/internal/tracing"
|
||||
)
|
||||
|
||||
// ociCredsPolicyBuilder is the type of a callback function that the [providerSource]
|
||||
@@ -72,21 +75,34 @@ func getOCIRepositoryStore(ctx context.Context, registryDomain, repositoryName s
|
||||
return store, nil
|
||||
}
|
||||
|
||||
ctx, span := tracing.Tracer().Start(
|
||||
ctx, "Authenticate to OCI Registry",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String("opentofu.oci.registry.domain", registryDomain),
|
||||
otelAttr.String("opentofu.oci.repository.name", repositoryName),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
client, err := getOCIRepositoryORASClient(ctx, registryDomain, repositoryName, credsPolicy)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return nil, err
|
||||
}
|
||||
reg, err := orasRemote.NewRegistry(registryDomain)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return nil, err // This is only for registryDomain validation errors, and we should've caught those much earlier than here
|
||||
}
|
||||
reg.Client = client
|
||||
err = reg.Ping(ctx) // tests whether the given domain refers to a valid OCI repository and will accept the credentials
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return nil, fmt.Errorf("failed to contact OCI registry at %q: %w", registryDomain, err)
|
||||
}
|
||||
repo, err := reg.Repository(ctx, repositoryName)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return nil, err // This is only for repositoryName validation errors, and we should've caught those much earlier than here
|
||||
}
|
||||
|
||||
@@ -160,6 +176,15 @@ func (o ociCredentialsLookupEnv) QueryDockerCredentialHelper(ctx context.Context
|
||||
// (just because this type name is very long to keep repeating in full)
|
||||
type Result = ociauthconfig.DockerCredentialHelperGetResult
|
||||
|
||||
ctx, span := tracing.Tracer().Start(
|
||||
ctx, "Query Docker-style credential helper",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String("opentofu.oci.docker_credential_helper.name", helperName),
|
||||
otelAttr.String("opentofu.oci.registry.url", serverURL),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
// We currently use the ORAS-Go implementation of the Docker
|
||||
// credential helper protocol, because we already depend on
|
||||
// that library for our OCI registry interactions elsewhere.
|
||||
@@ -167,11 +192,23 @@ func (o ociCredentialsLookupEnv) QueryDockerCredentialHelper(ctx context.Context
|
||||
// than "Docker-style Credential Helper", but it's the
|
||||
// same protocol nonetheless.
|
||||
|
||||
var executeSpan otelTrace.Span // ORAS tracing API can't directly propagate span from Start to Done
|
||||
ctx = orasCredsTrace.WithExecutableTrace(ctx, &orasCredsTrace.ExecutableTrace{
|
||||
ExecuteStart: func(executableName, action string) {
|
||||
_, executeSpan = tracing.Tracer().Start(
|
||||
ctx, "Execute helper program",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String("opentofu.oci.docker_credential_helper.executable", helperName),
|
||||
otelAttr.String("opentofu.oci.registry.url", serverURL),
|
||||
),
|
||||
)
|
||||
log.Printf("[DEBUG] Executing docker-style credentials helper %q for %s", helperName, serverURL)
|
||||
},
|
||||
ExecuteDone: func(executableName, action string, err error) {
|
||||
if executeSpan != nil {
|
||||
tracing.SetSpanError(executeSpan, err)
|
||||
executeSpan.End()
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Docker-style credential helper %q failed for %s: %s", helperName, serverURL, err)
|
||||
}
|
||||
@@ -181,6 +218,7 @@ func (o ociCredentialsLookupEnv) QueryDockerCredentialHelper(ctx context.Context
|
||||
store := orasCreds.NewNativeStore(helperName)
|
||||
creds, err := store.Get(ctx, serverURL)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return Result{}, fmt.Errorf("%q credential helper failed: %w", helperName, err)
|
||||
}
|
||||
if creds.AccessToken != "" || creds.RefreshToken != "" {
|
||||
@@ -190,7 +228,9 @@ func (o ociCredentialsLookupEnv) QueryDockerCredentialHelper(ctx context.Context
|
||||
// username/password style. So for completeness/robustness we check
|
||||
// the OAuth fields and fail if they are set, but it should not actually
|
||||
// be possible for them to be set in practice.
|
||||
return Result{}, fmt.Errorf("%q credential helper returned OAuth-style credentials, but only username/password-style is allowed from a credential helper", helperName)
|
||||
err := fmt.Errorf("%q credential helper returned OAuth-style credentials, but only username/password-style is allowed from a credential helper", helperName)
|
||||
tracing.SetSpanError(span, err)
|
||||
return Result{}, err
|
||||
}
|
||||
return Result{
|
||||
ServerURL: serverURL,
|
||||
|
||||
@@ -17,6 +17,9 @@ import (
|
||||
getter "github.com/hashicorp/go-getter"
|
||||
ociDigest "github.com/opencontainers/go-digest"
|
||||
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/opentofu/opentofu/internal/tracing"
|
||||
otelAttr "go.opentelemetry.io/otel/attribute"
|
||||
otelTrace "go.opentelemetry.io/otel/trace"
|
||||
orasContent "oras.land/oras-go/v2/content"
|
||||
orasRegistry "oras.land/oras-go/v2/registry"
|
||||
)
|
||||
@@ -66,34 +69,52 @@ var _ getter.Getter = (*ociDistributionGetter)(nil)
|
||||
func (g *ociDistributionGetter) Get(destDir string, url *url.URL) error {
|
||||
ctx := g.context()
|
||||
|
||||
ctx, span := tracing.Tracer().Start(
|
||||
ctx, "Fetch 'oci' module package",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String("opentofu.module.source", url.String()),
|
||||
otelAttr.String("opentofu.module.local_dir", destDir),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
ref, err := g.resolveRepositoryRef(url)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
store, err := g.getOCIRepositoryStore(ctx, ref.Registry, ref.Repository)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configuring client for %s: %w", ref, err)
|
||||
err := fmt.Errorf("configuring client for %s: %w", ref, err)
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
manifestDesc, err := g.resolveManifestDescriptor(ctx, ref, url.Query(), store)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
manifest, err := fetchOCIImageManifest(ctx, manifestDesc, store)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
pkgDesc, err := selectOCILayerBlob(manifest.Layers)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
decompKey := goGetterDecompressorMediaTypes[pkgDesc.MediaType]
|
||||
decomp := goGetterDecompressors[decompKey]
|
||||
if decomp == nil {
|
||||
// Should not get here if selectOCILayerBlob is implemented correctly.
|
||||
return fmt.Errorf("no decompressor available for media type %q", pkgDesc.MediaType)
|
||||
err := fmt.Errorf("no decompressor available for media type %q", pkgDesc.MediaType)
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
tempFile, err := fetchOCIBlobToTemporaryFile(ctx, pkgDesc, store)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tempFile)
|
||||
@@ -104,7 +125,9 @@ func (g *ociDistributionGetter) Get(destDir string, url *url.URL) error {
|
||||
}
|
||||
err = decomp.Decompress(destDir, tempFile, true, umask)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decompressing package into %s: %w", destDir, err)
|
||||
err := fmt.Errorf("decompressing package into %s: %w", destDir, err)
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -195,32 +218,45 @@ func (g *ociDistributionGetter) resolveRepositoryRef(url *url.URL) (*orasRegistr
|
||||
}
|
||||
|
||||
func (g *ociDistributionGetter) resolveManifestDescriptor(ctx context.Context, ref *orasRegistry.Reference, query url.Values, store OCIRepositoryStore) (desc ociv1.Descriptor, err error) {
|
||||
ctx, span := tracing.Tracer().Start(
|
||||
ctx, "Resolve reference",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String("opentofu.oci.registry.domain", ref.Registry),
|
||||
otelAttr.String("opentofu.oci.repository.name", ref.Repository),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
prepErr := func(err error) error {
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
|
||||
var unsupportedArgs []string
|
||||
var wantTag string
|
||||
var wantDigest ociDigest.Digest
|
||||
for name, values := range query {
|
||||
if len(values) > 1 {
|
||||
return ociv1.Descriptor{}, fmt.Errorf("too many %q arguments", name)
|
||||
return ociv1.Descriptor{}, prepErr(fmt.Errorf("too many %q arguments", name))
|
||||
}
|
||||
value := values[0]
|
||||
switch name {
|
||||
case "tag":
|
||||
if value == "" {
|
||||
return ociv1.Descriptor{}, fmt.Errorf("tag argument must not be empty")
|
||||
return ociv1.Descriptor{}, prepErr(fmt.Errorf("tag argument must not be empty"))
|
||||
}
|
||||
tagRef := *ref // shallow copy so we can modify the reference field
|
||||
tagRef.Reference = value // We'll again borrow the ORAS-Go validation for this
|
||||
if err := tagRef.ValidateReferenceAsTag(); err != nil {
|
||||
return ociv1.Descriptor{}, err // message includes suitable context prefix already
|
||||
return ociv1.Descriptor{}, prepErr(err) // message includes suitable context prefix already
|
||||
}
|
||||
wantTag = value
|
||||
case "digest":
|
||||
if value == "" {
|
||||
return ociv1.Descriptor{}, fmt.Errorf("digest argument must not be empty")
|
||||
return ociv1.Descriptor{}, prepErr(fmt.Errorf("digest argument must not be empty"))
|
||||
}
|
||||
d, err := ociDigest.Parse(value)
|
||||
if err != nil {
|
||||
return ociv1.Descriptor{}, fmt.Errorf("invalid digest: %s", err)
|
||||
return ociv1.Descriptor{}, prepErr(fmt.Errorf("invalid digest: %s", err))
|
||||
}
|
||||
wantDigest = d
|
||||
default:
|
||||
@@ -228,12 +264,12 @@ func (g *ociDistributionGetter) resolveManifestDescriptor(ctx context.Context, r
|
||||
}
|
||||
}
|
||||
if len(unsupportedArgs) == 1 {
|
||||
return ociv1.Descriptor{}, fmt.Errorf("unsupported argument %q", unsupportedArgs[0])
|
||||
return ociv1.Descriptor{}, prepErr(fmt.Errorf("unsupported argument %q", unsupportedArgs[0]))
|
||||
} else if len(unsupportedArgs) >= 2 {
|
||||
return ociv1.Descriptor{}, fmt.Errorf("unsupported arguments: %s", strings.Join(unsupportedArgs, ", "))
|
||||
return ociv1.Descriptor{}, prepErr(fmt.Errorf("unsupported arguments: %s", strings.Join(unsupportedArgs, ", ")))
|
||||
}
|
||||
if wantTag != "" && wantDigest != "" {
|
||||
return ociv1.Descriptor{}, fmt.Errorf("cannot set both \"tag\" and \"digest\" arguments")
|
||||
return ociv1.Descriptor{}, prepErr(fmt.Errorf("cannot set both \"tag\" and \"digest\" arguments"))
|
||||
}
|
||||
if wantTag == "" && wantDigest == "" {
|
||||
wantTag = "latest" // default tag to use if no arguments are present
|
||||
@@ -242,9 +278,12 @@ func (g *ociDistributionGetter) resolveManifestDescriptor(ctx context.Context, r
|
||||
if wantTag != "" {
|
||||
// If we're starting with a tag name then we need to query the
|
||||
// repository to find out which digest is currently selected.
|
||||
span.SetAttributes(
|
||||
otelAttr.String("opentofu.oci.reference.tag", wantTag),
|
||||
)
|
||||
desc, err = store.Resolve(ctx, wantTag)
|
||||
if err != nil {
|
||||
return ociv1.Descriptor{}, fmt.Errorf("resolving tag %q: %w", wantTag, err)
|
||||
return ociv1.Descriptor{}, prepErr(fmt.Errorf("resolving tag %q: %w", wantTag, err))
|
||||
}
|
||||
} else {
|
||||
// If we're requesting a specific digest then we still need to
|
||||
@@ -254,18 +293,27 @@ func (g *ociDistributionGetter) resolveManifestDescriptor(ctx context.Context, r
|
||||
// most of the other implementations only allow resolving by tag,
|
||||
// and so we can't exercise this specific case from unit tests
|
||||
// using in-memory or on-disk fakes. :(
|
||||
span.SetAttributes(
|
||||
otelAttr.String("opentofu.oci.reference.digest", wantDigest.String()),
|
||||
)
|
||||
desc, err = store.Resolve(ctx, wantDigest.String())
|
||||
if err != nil {
|
||||
return ociv1.Descriptor{}, fmt.Errorf("resolving digest %q: %w", wantDigest, err)
|
||||
return ociv1.Descriptor{}, prepErr(fmt.Errorf("resolving digest %q: %w", wantDigest, err))
|
||||
}
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
otelAttr.String("oci.manifest.digest", desc.Digest.String()),
|
||||
otelAttr.String("opentofu.oci.manifest.media_type", desc.MediaType),
|
||||
otelAttr.Int64("opentofu.oci.manifest.size", desc.Size),
|
||||
)
|
||||
|
||||
// The initial request is only required to return a "plain" descriptor,
|
||||
// with only MediaType+Digest+Size, so we can verify the media type
|
||||
// here but we'll need to wait until we fetch the manifest to verify
|
||||
// the ArtifactType and any other details.
|
||||
if desc.MediaType != ociv1.MediaTypeImageManifest {
|
||||
return ociv1.Descriptor{}, fmt.Errorf("selected object is not an OCI image manifest")
|
||||
return ociv1.Descriptor{}, prepErr(fmt.Errorf("selected object is not an OCI image manifest"))
|
||||
}
|
||||
|
||||
// We always expect ArtifactType to be set to our OpenTofu-specific type,
|
||||
@@ -276,9 +324,22 @@ func (g *ociDistributionGetter) resolveManifestDescriptor(ctx context.Context, r
|
||||
}
|
||||
|
||||
func fetchOCIImageManifest(ctx context.Context, desc ociv1.Descriptor, store OCIRepositoryStore) (*ociv1.Manifest, error) {
|
||||
ctx, span := tracing.Tracer().Start(
|
||||
ctx, "Fetch manifest",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String("oci.manifest.digest", desc.Digest.String()),
|
||||
otelAttr.Int64("opentofu.oci.manifest.size", desc.Size),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
prepErr := func(err error) error {
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
|
||||
manifestSrc, err := fetchOCIManifestBlob(ctx, desc, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, prepErr(err)
|
||||
}
|
||||
|
||||
var manifest ociv1.Manifest
|
||||
@@ -289,21 +350,26 @@ func fetchOCIImageManifest(ctx context.Context, desc ociv1.Descriptor, store OCI
|
||||
// failure could prevent us from reaching the MediaType check below.
|
||||
var manifest ociv1.Index
|
||||
if err := json.Unmarshal(manifestSrc, &manifest); err == nil && manifest.MediaType == ociv1.MediaTypeImageIndex {
|
||||
return nil, fmt.Errorf("found index manifest but need image manifest")
|
||||
return nil, prepErr(fmt.Errorf("found index manifest but need image manifest"))
|
||||
}
|
||||
return nil, fmt.Errorf("invalid manifest content: %w", err)
|
||||
return nil, prepErr(fmt.Errorf("invalid manifest content: %w", err))
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
otelAttr.String("opentofu.oci.manifest.media_type", desc.MediaType),
|
||||
otelAttr.String("opentofu.oci.manifest.artifact_type", desc.ArtifactType),
|
||||
)
|
||||
|
||||
// Now we'll make sure that what we decoded seems vaguely sensible before we
|
||||
// return it. Callers are allowed to rely on these checks by verifying
|
||||
// that their provided descriptor specifies the wanted media and artifact
|
||||
// types before they call this function and then assuming that the result
|
||||
// definitely matches what they asked for.
|
||||
if manifest.MediaType != desc.MediaType {
|
||||
return nil, fmt.Errorf("unexpected manifest media type %q", manifest.MediaType)
|
||||
return nil, prepErr(fmt.Errorf("unexpected manifest media type %q", manifest.MediaType))
|
||||
}
|
||||
if manifest.ArtifactType != desc.ArtifactType {
|
||||
return nil, fmt.Errorf("unexpected artifact type %q", manifest.ArtifactType)
|
||||
return nil, prepErr(fmt.Errorf("unexpected artifact type %q", manifest.ArtifactType))
|
||||
}
|
||||
// We intentionally leave everything else loose so that we'll have flexibility
|
||||
// to extend this format in backward-compatible ways in future OpenTofu versions.
|
||||
@@ -386,12 +452,25 @@ func selectOCILayerBlob(descs []ociv1.Descriptor) (ociv1.Descriptor, error) {
|
||||
// It is the caller's responsibility to delete the temporary file once it's no longer
|
||||
// needed.
|
||||
func fetchOCIBlobToTemporaryFile(ctx context.Context, desc ociv1.Descriptor, store orasContent.Fetcher) (tempFile string, err error) {
|
||||
ctx, span := tracing.Tracer().Start(
|
||||
ctx, "Fetch module package",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String("opentofu.oci.blob.digest", desc.Digest.String()),
|
||||
otelAttr.String("opentofu.oci.blob.media_type", desc.MediaType),
|
||||
otelAttr.Int64("opentofu.oci.blob.size", desc.Size),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
f, err := os.CreateTemp("", "opentofu-module")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open temporary file: %w", err)
|
||||
err := fmt.Errorf("failed to open temporary file: %w", err)
|
||||
tracing.SetSpanError(span, err)
|
||||
return "", err
|
||||
}
|
||||
tempFile = f.Name()
|
||||
defer func() {
|
||||
tracing.SetSpanError(span, err)
|
||||
// If we're returning an error then the caller won't make use of the
|
||||
// file we've created, so we'll make a best effort to proactively
|
||||
// remove it. If we return a nil error then it's the caller's
|
||||
|
||||
@@ -16,10 +16,14 @@ import (
|
||||
|
||||
"github.com/apparentlymart/go-versions/versions"
|
||||
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
otelAttr "go.opentelemetry.io/otel/attribute"
|
||||
otelTrace "go.opentelemetry.io/otel/trace"
|
||||
orasErrors "oras.land/oras-go/v2/errdef"
|
||||
orasRegistryErrors "oras.land/oras-go/v2/registry/remote/errcode"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/tracing"
|
||||
"github.com/opentofu/opentofu/internal/tracing/traceattrs"
|
||||
)
|
||||
|
||||
// ociIndexManifestArtifactType is the artifact type we expect for the top-level
|
||||
@@ -156,8 +160,17 @@ func NewOCIRegistryMirrorSource(
|
||||
|
||||
// AvailableVersions implements Source.
|
||||
func (o *OCIRegistryMirrorSource) AvailableVersions(ctx context.Context, provider addrs.Provider) (VersionList, Warnings, error) {
|
||||
ctx, span := tracing.Tracer().Start(
|
||||
ctx, "Get available versions from oci_mirror",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String(traceattrs.ProviderAddress, provider.String()),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
store, _, _, err := o.getRepositoryStore(ctx, provider)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -190,11 +203,15 @@ func (o *OCIRegistryMirrorSource) AvailableVersions(ctx context.Context, provide
|
||||
// translated to an OCI repository that doesn't exist then that
|
||||
// means this source does not know anything about that provider,
|
||||
// but other [MultiSource] candidates should still be offered it.
|
||||
return nil, nil, ErrProviderNotFound{
|
||||
err := ErrProviderNotFound{
|
||||
Provider: provider,
|
||||
}
|
||||
tracing.SetSpanError(span, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, nil, fmt.Errorf("listing tags from OCI repository: %w", err)
|
||||
err := fmt.Errorf("listing tags from OCI repository: %w", err)
|
||||
tracing.SetSpanError(span, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
ret.Sort()
|
||||
return ret, nil, nil
|
||||
@@ -258,6 +275,16 @@ func errRepresentsOCIProviderNotFound(err error) bool {
|
||||
|
||||
// PackageMeta implements Source.
|
||||
func (o *OCIRegistryMirrorSource) PackageMeta(ctx context.Context, provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
|
||||
ctx, span := tracing.Tracer().Start(
|
||||
ctx, "Get metadata from oci_mirror",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String(traceattrs.ProviderAddress, provider.String()),
|
||||
otelAttr.String(traceattrs.ProviderVersion, version.String()),
|
||||
otelAttr.String(traceattrs.TargetPlatform, target.String()),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
|
||||
// Unfortunately we need to repeat our translation from provider address to
|
||||
// OCI repository address here, but getRepositoryStore has a cache that
|
||||
// allows us to reuse a previously-instantiated store if there are two
|
||||
@@ -446,14 +473,31 @@ type OCIRepositoryStore interface {
|
||||
}
|
||||
|
||||
func fetchOCIDescriptorForVersion(ctx context.Context, version versions.Version, store OCIRepositoryStore) (ociv1.Descriptor, error) {
|
||||
ctx, span := tracing.Tracer().Start(
|
||||
ctx, "Resolve reference",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String(traceattrs.ProviderVersion, version.String()),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
prepErr := func(err error) error {
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// OCI tags don't support the "+" character used to mark the beginning of
|
||||
// build metadata in semver-style version numbers, so we expect a tag name
|
||||
// where those are replaced with "_".
|
||||
tagName := strings.ReplaceAll(version.String(), "+", "_")
|
||||
desc, err := store.Resolve(ctx, tagName)
|
||||
if err != nil {
|
||||
return ociv1.Descriptor{}, fmt.Errorf("resolving tag %q: %w", tagName, err)
|
||||
return ociv1.Descriptor{}, prepErr(fmt.Errorf("resolving tag %q: %w", tagName, err))
|
||||
}
|
||||
span.SetAttributes(
|
||||
otelAttr.String("oci.manifest.digest", desc.Digest.String()),
|
||||
otelAttr.String("opentofu.oci.reference.tag", tagName),
|
||||
otelAttr.String("opentofu.oci.manifest.media_type", desc.MediaType),
|
||||
)
|
||||
// Not all store implementations can return the manifest's artifact type as part
|
||||
// of the tag-resolution response, so we'll check this early if we can, but
|
||||
// if not then we'll check it after we've fetched the manifest instead.
|
||||
@@ -461,31 +505,34 @@ func fetchOCIDescriptorForVersion(ctx context.Context, version versions.Version,
|
||||
// a better error message, but we'll still catch a mismatched artifact type
|
||||
// one way or another.)
|
||||
if desc.ArtifactType != "" && desc.ArtifactType != ociIndexManifestArtifactType {
|
||||
span.SetAttributes(
|
||||
otelAttr.String("opentofu.oci.manifest.artifact_type", desc.ArtifactType),
|
||||
)
|
||||
switch desc.ArtifactType {
|
||||
case "application/vnd.opentofu.provider-target":
|
||||
// We'll get here for an incorrectly-constructed artifact layout where
|
||||
// the tag refers directly to a specific platform's image manifest,
|
||||
// rather than to the multi-platform index manifest.
|
||||
return desc, fmt.Errorf("tag refers directly to image manifest, but OpenTofu providers require an index manifest for multi-platform support")
|
||||
return desc, prepErr(fmt.Errorf("tag refers directly to image manifest, but OpenTofu providers require an index manifest for multi-platform support"))
|
||||
case "application/vnd.opentofu.modulepkg":
|
||||
// If this happens to be using our artifact type for module packages then
|
||||
// we'll return a specialized error, since confusion between providers
|
||||
// and modules is common for those new to OpenTofu terminology.
|
||||
return desc, fmt.Errorf("selected OCI artifact is an OpenTofu module package, not a provider package")
|
||||
return desc, prepErr(fmt.Errorf("selected OCI artifact is an OpenTofu module package, not a provider package"))
|
||||
default:
|
||||
// For any other artifact type we'll just mention it in the error message
|
||||
// and hope the reader can figure out what that artifact type represents.
|
||||
return desc, fmt.Errorf("unsupported OCI artifact type %q", desc.ArtifactType)
|
||||
return desc, prepErr(fmt.Errorf("unsupported OCI artifact type %q", desc.ArtifactType))
|
||||
}
|
||||
}
|
||||
if desc.MediaType != ociv1.MediaTypeImageIndex {
|
||||
switch desc.MediaType {
|
||||
case ociv1.MediaTypeImageManifest:
|
||||
return desc, fmt.Errorf("selected an OCI image manifest directly, but providers must be selected through a multi-platform index manifest")
|
||||
return desc, prepErr(fmt.Errorf("selected an OCI image manifest directly, but providers must be selected through a multi-platform index manifest"))
|
||||
case ociv1.MediaTypeDescriptor:
|
||||
return desc, fmt.Errorf("found OCI descriptor but expected multi-platform index manifest")
|
||||
return desc, prepErr(fmt.Errorf("found OCI descriptor but expected multi-platform index manifest"))
|
||||
default:
|
||||
return desc, fmt.Errorf("unsupported media type %q for OCI index manifest", desc.MediaType)
|
||||
return desc, prepErr(fmt.Errorf("unsupported media type %q for OCI index manifest", desc.MediaType))
|
||||
}
|
||||
}
|
||||
// If the server didn't tell us the artifact type it has, then we'll populate
|
||||
@@ -496,9 +543,22 @@ func fetchOCIDescriptorForVersion(ctx context.Context, version versions.Version,
|
||||
}
|
||||
|
||||
func fetchOCIIndexManifest(ctx context.Context, desc ociv1.Descriptor, store OCIRepositoryStore) (*ociv1.Index, error) {
|
||||
ctx, span := tracing.Tracer().Start(
|
||||
ctx, "Fetch index manifest",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String("oci.manifest.digest", desc.Digest.String()),
|
||||
otelAttr.Int64("opentofu.oci.manifest.size", desc.Size),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
prepErr := func(err error) error {
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
|
||||
manifestSrc, err := fetchOCIManifestBlob(ctx, desc, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, prepErr(err)
|
||||
}
|
||||
|
||||
var manifest ociv1.Index
|
||||
@@ -509,10 +569,14 @@ func fetchOCIIndexManifest(ctx context.Context, desc ociv1.Descriptor, store OCI
|
||||
// failure could prevent us from reaching the MediaType check below.
|
||||
var manifest ociv1.Manifest
|
||||
if err := json.Unmarshal(manifestSrc, &manifest); err == nil && manifest.MediaType == ociv1.MediaTypeImageManifest {
|
||||
return nil, fmt.Errorf("found image manifest but need index manifest")
|
||||
return nil, prepErr(fmt.Errorf("found image manifest but need index manifest"))
|
||||
}
|
||||
return nil, fmt.Errorf("invalid manifest content: %w", err)
|
||||
return nil, prepErr(fmt.Errorf("invalid manifest content: %w", err))
|
||||
}
|
||||
span.SetAttributes(
|
||||
otelAttr.String("opentofu.oci.manifest.media_type", manifest.MediaType),
|
||||
otelAttr.String("opentofu.oci.manifest.artifact_type", manifest.ArtifactType),
|
||||
)
|
||||
|
||||
// Now we'll make sure that what we decoded seems vaguely sensible before we
|
||||
// return it. Callers are allowed to rely on these checks by verifying
|
||||
@@ -520,10 +584,10 @@ func fetchOCIIndexManifest(ctx context.Context, desc ociv1.Descriptor, store OCI
|
||||
// types before they call this function and then assuming that the result
|
||||
// definitely matches what they asked for.
|
||||
if manifest.MediaType != desc.MediaType {
|
||||
return nil, fmt.Errorf("unexpected manifest media type %q", manifest.MediaType)
|
||||
return nil, prepErr(fmt.Errorf("unexpected manifest media type %q", manifest.MediaType))
|
||||
}
|
||||
if manifest.ArtifactType != desc.ArtifactType {
|
||||
return nil, fmt.Errorf("unexpected artifact type %q", manifest.ArtifactType)
|
||||
return nil, prepErr(fmt.Errorf("unexpected artifact type %q", manifest.ArtifactType))
|
||||
}
|
||||
// We intentionally leave everything else loose so that we'll have flexibility
|
||||
// to extend this format in backward-compatible ways in future OpenTofu versions.
|
||||
@@ -531,9 +595,22 @@ func fetchOCIIndexManifest(ctx context.Context, desc ociv1.Descriptor, store OCI
|
||||
}
|
||||
|
||||
func fetchOCIImageManifest(ctx context.Context, desc ociv1.Descriptor, store OCIRepositoryStore) (*ociv1.Manifest, error) {
|
||||
ctx, span := tracing.Tracer().Start(
|
||||
ctx, "Fetch platform-specific manifest",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String("oci.manifest.digest", desc.Digest.String()),
|
||||
otelAttr.Int64("opentofu.oci.manifest.size", desc.Size),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
prepErr := func(err error) error {
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
|
||||
manifestSrc, err := fetchOCIManifestBlob(ctx, desc, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, prepErr(err)
|
||||
}
|
||||
|
||||
var manifest ociv1.Manifest
|
||||
@@ -544,10 +621,14 @@ func fetchOCIImageManifest(ctx context.Context, desc ociv1.Descriptor, store OCI
|
||||
// failure could prevent us from reaching the MediaType check below.
|
||||
var manifest ociv1.Index
|
||||
if err := json.Unmarshal(manifestSrc, &manifest); err == nil && manifest.MediaType == ociv1.MediaTypeImageIndex {
|
||||
return nil, fmt.Errorf("found index manifest but need image manifest")
|
||||
return nil, prepErr(fmt.Errorf("found index manifest but need image manifest"))
|
||||
}
|
||||
return nil, fmt.Errorf("invalid manifest content: %w", err)
|
||||
return nil, prepErr(fmt.Errorf("invalid manifest content: %w", err))
|
||||
}
|
||||
span.SetAttributes(
|
||||
otelAttr.String("opentofu.oci.manifest.media_type", manifest.MediaType),
|
||||
otelAttr.String("opentofu.oci.manifest.artifact_type", manifest.ArtifactType),
|
||||
)
|
||||
|
||||
// Now we'll make sure that what we decoded seems vaguely sensible before we
|
||||
// return it. Callers are allowed to rely on these checks by verifying
|
||||
@@ -555,10 +636,10 @@ func fetchOCIImageManifest(ctx context.Context, desc ociv1.Descriptor, store OCI
|
||||
// types before they call this function and then assuming that the result
|
||||
// definitely matches what they asked for.
|
||||
if manifest.MediaType != desc.MediaType {
|
||||
return nil, fmt.Errorf("unexpected manifest media type %q", manifest.MediaType)
|
||||
return nil, prepErr(fmt.Errorf("unexpected manifest media type %q", manifest.MediaType))
|
||||
}
|
||||
if manifest.ArtifactType != desc.ArtifactType {
|
||||
return nil, fmt.Errorf("unexpected artifact type %q", manifest.ArtifactType)
|
||||
return nil, prepErr(fmt.Errorf("unexpected artifact type %q", manifest.ArtifactType))
|
||||
}
|
||||
// We intentionally leave everything else loose so that we'll have flexibility
|
||||
// to extend this format in backward-compatible ways in future OpenTofu versions.
|
||||
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
"github.com/hashicorp/go-getter"
|
||||
ociDigest "github.com/opencontainers/go-digest"
|
||||
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/opentofu/opentofu/internal/tracing"
|
||||
"github.com/opentofu/opentofu/internal/tracing/traceattrs"
|
||||
otelAttr "go.opentelemetry.io/otel/attribute"
|
||||
otelTrace "go.opentelemetry.io/otel/trace"
|
||||
orasContent "oras.land/oras-go/v2/content"
|
||||
)
|
||||
|
||||
@@ -82,6 +86,25 @@ var _ PackageLocation = PackageOCIBlobArchive{}
|
||||
|
||||
func (p PackageOCIBlobArchive) InstallProviderPackage(ctx context.Context, meta PackageMeta, targetDir string, allowedHashes []Hash) (*PackageAuthenticationResult, error) {
|
||||
pkgDesc := p.blobDescriptor
|
||||
ctx, span := tracing.Tracer().Start(
|
||||
ctx, "Fetch provider package",
|
||||
otelTrace.WithAttributes(
|
||||
otelAttr.String("opentofu.oci.registry.domain", p.registryDomain),
|
||||
otelAttr.String("opentofu.oci.repository.name", p.repositoryName),
|
||||
otelAttr.String("opentofu.oci.blob.digest", pkgDesc.Digest.String()),
|
||||
otelAttr.String("opentofu.oci.blob.media_type", pkgDesc.MediaType),
|
||||
otelAttr.Int64("opentofu.oci.blob.size", pkgDesc.Size),
|
||||
otelAttr.String("opentofu.provider.local_dir", targetDir),
|
||||
otelAttr.String(traceattrs.ProviderAddress, meta.Provider.String()),
|
||||
otelAttr.String(traceattrs.ProviderVersion, meta.Version.String()),
|
||||
otelAttr.String(traceattrs.TargetPlatform, meta.TargetPlatform.String()),
|
||||
),
|
||||
)
|
||||
defer span.End()
|
||||
prepErr := func(err error) error {
|
||||
tracing.SetSpanError(span, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// First we'll make sure that what we've been given makes sense to be the descriptor
|
||||
// for a provider package blob. A failure here suggests a bug in the [Source] that
|
||||
@@ -89,13 +112,13 @@ func (p PackageOCIBlobArchive) InstallProviderPackage(ctx context.Context, meta
|
||||
// location type cannot support.
|
||||
err := checkOCIBlobDescriptor(pkgDesc, meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, prepErr(err)
|
||||
}
|
||||
|
||||
// If we have a fixed set of allowed hashes then we'll check that our
|
||||
// selected descriptor matches before we waste time fetching the package.
|
||||
if len(allowedHashes) > 0 && !ociPackageDescriptorDigestMatchesAnyHash(pkgDesc.Digest, allowedHashes) {
|
||||
return nil, fmt.Errorf(
|
||||
return nil, prepErr(fmt.Errorf(
|
||||
// FIXME: We currently have slightly-different variations of this error
|
||||
// message spread across the different [PackageLocation] implementations.
|
||||
// It would be good to settle on a single good version of this text,
|
||||
@@ -105,7 +128,7 @@ func (p PackageOCIBlobArchive) InstallProviderPackage(ctx context.Context, meta
|
||||
// separate task from implementing OCI-based installation as a new feature.
|
||||
"the current package for %s %s doesn't match any of the checksums previously recorded in the dependency lock file; for more information: https://opentofu.org/docs/language/files/dependency-lock/#checksum-verification",
|
||||
meta.Provider, meta.Version,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
// If we reach this point then we have a descriptor for what will hopefully
|
||||
@@ -114,7 +137,7 @@ func (p PackageOCIBlobArchive) InstallProviderPackage(ctx context.Context, meta
|
||||
// into its final location.
|
||||
localLoc, err := fetchOCIBlobToTemporaryFile(ctx, pkgDesc, p.repoStore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching provider package blob %s: %w", pkgDesc.Digest.String(), err)
|
||||
return nil, prepErr(fmt.Errorf("fetching provider package blob %s: %w", pkgDesc.Digest.String(), err))
|
||||
}
|
||||
defer os.Remove(string(localLoc)) // Best effort to remove the temporary file before we return
|
||||
|
||||
@@ -136,7 +159,8 @@ func (p PackageOCIBlobArchive) InstallProviderPackage(ctx context.Context, meta
|
||||
Location: localLoc,
|
||||
Authentication: meta.Authentication,
|
||||
}
|
||||
return localLoc.InstallProviderPackage(ctx, localMeta, targetDir, allowedHashes)
|
||||
authResult, err := localLoc.InstallProviderPackage(ctx, localMeta, targetDir, allowedHashes)
|
||||
return authResult, prepErr(err)
|
||||
}
|
||||
|
||||
func (p PackageOCIBlobArchive) String() string {
|
||||
|
||||
Reference in New Issue
Block a user