mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
main: Move the OCI Distribution-related helpers into their own file
These functions and types are about to become cross-cutting concerns shared between both the provider and module installers in a future commit, so we'll move them out to a separate file to hopefully communicate the relationships between these parts a little more clearly. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
119
cmd/tofu/oci_distribution.go
Normal file
119
cmd/tofu/oci_distribution.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package main
|
||||
|
||||
// This file deals with our cross-cutting concerns relating to the OCI Distribution
|
||||
// protocol, shared across both the provider and module installers, and potentially
|
||||
// other OCI Registry concerns in future.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
orasRemote "oras.land/oras-go/v2/registry/remote"
|
||||
orasAuth "oras.land/oras-go/v2/registry/remote/auth"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/command/cliconfig/ociauthconfig"
|
||||
"github.com/opentofu/opentofu/internal/getmodules"
|
||||
"github.com/opentofu/opentofu/internal/getproviders"
|
||||
"github.com/opentofu/opentofu/internal/httpclient"
|
||||
)
|
||||
|
||||
// ociCredsPolicyBuilder is the type of a callback function that the [providerSource]
|
||||
// and [remoteModulePackageFetcher] functions will use if any of the configured
|
||||
// provider installation methods or the module installer need to interact with
|
||||
// OCI Distribution registries.
|
||||
//
|
||||
// We represent this indirectly as a callback function so that we can skip doing
|
||||
// this work in the common case where we won't need to interact with OCI registries
|
||||
// at all.
|
||||
type ociCredsPolicyBuilder func(context.Context) (ociauthconfig.CredentialsConfigs, error)
|
||||
|
||||
// getOCIRepositoryStore instantiates a [getproviders.OCIRepositoryStore] implementation to use
|
||||
// when accessing the given repository on the given registry, using the given OCI credentials
|
||||
// policy to decide which credentials to use.
|
||||
func getOCIRepositoryStore(ctx context.Context, registryDomain, repositoryName string, credsPolicy ociauthconfig.CredentialsConfigs) (ociRepositoryStore, error) {
|
||||
// We currently use the ORAS-Go library to satisfy both the [getproviders.OCIRepositoryStore]
|
||||
// and [getmodules.OCIRepositoryStore] interfaces, which is easy because those interfaces
|
||||
// were designed to match a subset of the ORAS-Go API since we had no particular need to
|
||||
// diverge from it. However, we consider ORAS-Go to be an implementation detail here and so
|
||||
// we should avoid any ORAS-Go types becoming part of the direct public API between packages.
|
||||
|
||||
// ORAS-Go has a bit of an impedence mismatch with us in that it thinks of credentials
|
||||
// as being a per-registry thing rather than a per-repository thing, so we deal with
|
||||
// the credSource resolution ourselves here and then just return whatever we found to
|
||||
// ORAS when it asks through its callback. In practice we only interact with one
|
||||
// repository per client so this is just a little inconvenient and not a practical problem.
|
||||
credSource, err := credsPolicy.CredentialsSourceForRepository(ctx, registryDomain, repositoryName)
|
||||
if ociauthconfig.IsCredentialsNotFoundError(err) {
|
||||
credSource = nil // we'll just try without any credentials, then
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("finding credentials for %q: %w", registryDomain, err)
|
||||
}
|
||||
client := &orasAuth.Client{
|
||||
Client: httpclient.New(), // 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
|
||||
// other than the one they were configured for.
|
||||
return orasAuth.EmptyCredential, nil
|
||||
}
|
||||
if credSource == nil {
|
||||
return orasAuth.EmptyCredential, nil
|
||||
}
|
||||
creds, err := credSource.Credentials(ctx, ociCredentialsLookupEnv{})
|
||||
if ociauthconfig.IsCredentialsNotFoundError(err) {
|
||||
return orasAuth.EmptyCredential, nil
|
||||
}
|
||||
if err != nil {
|
||||
return orasAuth.Credential{}, err
|
||||
}
|
||||
return creds.ToORASCredential(), nil
|
||||
},
|
||||
Cache: orasAuth.NewCache(),
|
||||
}
|
||||
reg, err := orasRemote.NewRegistry(registryDomain)
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, fmt.Errorf("failed to contact OCI registry at %q: %w", registryDomain, err)
|
||||
}
|
||||
repo, err := reg.Repository(ctx, repositoryName)
|
||||
if err != nil {
|
||||
return nil, err // This is only for repositoryName validation errors, and we should've caught those much earlier than here
|
||||
}
|
||||
// NOTE: At this point we don't yet know if the named repository actually exists
|
||||
// in the registry. The caller will find that out when they try to interact
|
||||
// with the methods of the returned object.
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
// ociRepositoryStore represents the combined needs of both
|
||||
// [getproviders.OCIRepositoryStore] and [getmodules.OCIRepositoryStore],
|
||||
// both of which are intentionally defined to be subsets of the API
|
||||
// used by ORAS-Go so that we can use the implementations from that
|
||||
// library without directly exposing any ORAS-Go symbols in the
|
||||
// public API of any of our packages, since we want to reserve the
|
||||
// ability to switch to other implementations in future if needed.
|
||||
type ociRepositoryStore interface {
|
||||
getproviders.OCIRepositoryStore
|
||||
getmodules.OCIRepositoryStore
|
||||
}
|
||||
|
||||
// ociCredentialsLookupEnv is our implementation of ociauthconfig.CredentialsLookupEnvironment
|
||||
// used when resolving the selected credentials for a particular OCI repository.
|
||||
type ociCredentialsLookupEnv struct{}
|
||||
|
||||
var _ ociauthconfig.CredentialsLookupEnvironment = ociCredentialsLookupEnv{}
|
||||
|
||||
// QueryDockerCredentialHelper implements ociauthconfig.CredentialsLookupEnvironment.
|
||||
func (o ociCredentialsLookupEnv) QueryDockerCredentialHelper(ctx context.Context, helperName string, serverURL string) (ociauthconfig.DockerCredentialHelperGetResult, error) {
|
||||
// TODO: Implement this
|
||||
return ociauthconfig.DockerCredentialHelperGetResult{}, fmt.Errorf("support for Docker-style credential helpers is not yet available")
|
||||
}
|
||||
@@ -15,26 +15,13 @@ import (
|
||||
|
||||
"github.com/apparentlymart/go-userdirs/userdirs"
|
||||
"github.com/hashicorp/terraform-svchost/disco"
|
||||
orasRemote "oras.land/oras-go/v2/registry/remote"
|
||||
orasAuth "oras.land/oras-go/v2/registry/remote/auth"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/command/cliconfig"
|
||||
"github.com/opentofu/opentofu/internal/command/cliconfig/ociauthconfig"
|
||||
"github.com/opentofu/opentofu/internal/getproviders"
|
||||
"github.com/opentofu/opentofu/internal/httpclient"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
)
|
||||
|
||||
// ociCredsPolicyBuilder is the type of a callback function that the [providerSource]
|
||||
// functions will use if any of the configured provider installation methods
|
||||
// need to interact with OCI Distribution registries.
|
||||
//
|
||||
// We represent this indirectly as a callback function so that we can skip doing
|
||||
// this work in the common case where we won't need to interact with OCI registries
|
||||
// at all.
|
||||
type ociCredsPolicyBuilder func(context.Context) (ociauthconfig.CredentialsConfigs, error)
|
||||
|
||||
// providerSource constructs a provider source based on a combination of the
|
||||
// CLI configuration and some default search locations. This will be the
|
||||
// provider source used for provider installation in the "tofu init"
|
||||
@@ -277,77 +264,3 @@ func providerDevOverrides(configs []*cliconfig.ProviderInstallation) map[addrs.P
|
||||
// ignore any additional configurations in here.
|
||||
return configs[0].DevOverrides
|
||||
}
|
||||
|
||||
// getOCIRepositoryStore instantiates a [getproviders.OCIRepositoryStore] implementation to use
|
||||
// when accessing the given repository on the given registry, using the given OCI credentials
|
||||
// policy to decide which credentials to use.
|
||||
func getOCIRepositoryStore(ctx context.Context, registryDomain, repositoryName string, credsPolicy ociauthconfig.CredentialsConfigs) (getproviders.OCIRepositoryStore, error) {
|
||||
// We currently use the ORAS-Go library to satisfy the [getproviders.OCIRepositoryStore]
|
||||
// interface, which is easy because that interface was designed to match a subset of
|
||||
// the ORAS-Go API since we had no particular need to diverge from it. However, we consider
|
||||
// ORAS-Go to be an implementation detail here and so we should avoid any ORAS-Go
|
||||
// types becoming part of the direct public API between packages.
|
||||
|
||||
// ORAS-Go has a bit of an impedence mismatch with us in that it thinks of credentials
|
||||
// as being a per-registry thing rather than a per-repository thing, so we deal with
|
||||
// the credSource resolution ourselves here and then just return whatever we found to
|
||||
// ORAS when it asks through its callback. In practice we only interact with one
|
||||
// repository per client so this is just a little inconvenient and not a practical problem.
|
||||
credSource, err := credsPolicy.CredentialsSourceForRepository(ctx, registryDomain, repositoryName)
|
||||
if ociauthconfig.IsCredentialsNotFoundError(err) {
|
||||
credSource = nil // we'll just try without any credentials, then
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("finding credentials for %q: %w", registryDomain, err)
|
||||
}
|
||||
client := &orasAuth.Client{
|
||||
Client: httpclient.New(), // 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
|
||||
// other than the one they were configured for.
|
||||
return orasAuth.EmptyCredential, nil
|
||||
}
|
||||
if credSource == nil {
|
||||
return orasAuth.EmptyCredential, nil
|
||||
}
|
||||
creds, err := credSource.Credentials(ctx, ociCredentialsLookupEnv{})
|
||||
if ociauthconfig.IsCredentialsNotFoundError(err) {
|
||||
return orasAuth.EmptyCredential, nil
|
||||
}
|
||||
if err != nil {
|
||||
return orasAuth.Credential{}, err
|
||||
}
|
||||
return creds.ToORASCredential(), nil
|
||||
},
|
||||
Cache: orasAuth.NewCache(),
|
||||
}
|
||||
reg, err := orasRemote.NewRegistry(registryDomain)
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, fmt.Errorf("failed to contact OCI registry at %q: %w", registryDomain, err)
|
||||
}
|
||||
repo, err := reg.Repository(ctx, repositoryName)
|
||||
if err != nil {
|
||||
return nil, err // This is only for repositoryName validation errors, and we should've caught those much earlier than here
|
||||
}
|
||||
// NOTE: At this point we don't yet know if the named repository actually exists
|
||||
// in the registry. The caller will find that out when they try to interact
|
||||
// with the methods of [getproviders.OCIRepositoryStore].
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
// ociCredentialsLookupEnv is our implementation of ociauthconfig.CredentialsLookupEnvironment
|
||||
// used when resolving the selected credentials for a particular OCI repository.
|
||||
type ociCredentialsLookupEnv struct{}
|
||||
|
||||
var _ ociauthconfig.CredentialsLookupEnvironment = ociCredentialsLookupEnv{}
|
||||
|
||||
// QueryDockerCredentialHelper implements ociauthconfig.CredentialsLookupEnvironment.
|
||||
func (o ociCredentialsLookupEnv) QueryDockerCredentialHelper(ctx context.Context, helperName string, serverURL string) (ociauthconfig.DockerCredentialHelperGetResult, error) {
|
||||
// TODO: Implement this
|
||||
return ociauthconfig.DockerCredentialHelperGetResult{}, fmt.Errorf("support for Docker-style credential helpers is not yet available")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user