mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
cliconfig+main: Allow oci_mirror as a new provider installation method
It's now valid to include an oci_mirror block in the provider_installation block in the CLI configuration, specifying that OpenTofu should try to install providers from OCI repositories based on a template that maps from OpenTofu-style provider source addresses into OCI repository addresses. The getproviders.Source implementation for this was added in a previous commit, so this is mainly just wiring it up to the cliconfig layer and the dependency wiring code in package main. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
This commit is contained in:
@@ -15,11 +15,14 @@ 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"
|
||||
)
|
||||
|
||||
@@ -203,7 +206,7 @@ func implicitProviderSource(services *disco.Disco) getproviders.Source {
|
||||
return getproviders.MultiSource(searchRules)
|
||||
}
|
||||
|
||||
func providerSourceForCLIConfigLocation(loc cliconfig.ProviderInstallationLocation, services *disco.Disco, _ ociCredsPolicyBuilder) (getproviders.Source, tfdiags.Diagnostics) {
|
||||
func providerSourceForCLIConfigLocation(loc cliconfig.ProviderInstallationLocation, services *disco.Disco, makeOCICredsPolicy ociCredsPolicyBuilder) (getproviders.Source, tfdiags.Diagnostics) {
|
||||
if loc == cliconfig.ProviderInstallationDirect {
|
||||
return getproviders.NewMemoizeSource(
|
||||
getproviders.NewRegistrySource(services),
|
||||
@@ -237,11 +240,24 @@ func providerSourceForCLIConfigLocation(loc cliconfig.ProviderInstallationLocati
|
||||
}
|
||||
return getproviders.NewHTTPMirrorSource(url, services.CredentialsSource()), nil
|
||||
|
||||
// TODO: Once we implement an OCI-Distribution-based mirror source in a
|
||||
// future commit, we'll use the ociCredsPolicyBuilder callback as part of
|
||||
// initializing it so that it can find any credentials it needs to do its work.
|
||||
// For now this is just a stub to illustrate where future work should
|
||||
// continue, to help split this OCI integration work across multiple changes.
|
||||
case cliconfig.ProviderInstallationOCIMirror:
|
||||
mappingFunc := loc.RepositoryMapping
|
||||
return getproviders.NewOCIRegistryMirrorSource(
|
||||
mappingFunc,
|
||||
func(ctx context.Context, registryDomain, repositoryName string) (getproviders.OCIRepositoryStore, error) {
|
||||
// We intentionally delay the finalization of the credentials policy until
|
||||
// just before we need it because most OpenTofu commands don't install
|
||||
// providers at all, and even those that do only need to do this if
|
||||
// actually interacting with an OCI mirror, so we can avoid doing
|
||||
// this work at all most of the time.
|
||||
credsPolicy, err := makeOCICredsPolicy(ctx)
|
||||
if err != nil {
|
||||
// This deals with only a small number of errors that we can't catch during CLI config validation
|
||||
return nil, fmt.Errorf("invalid credentials configuration for OCI registries: %w", err)
|
||||
}
|
||||
return getOCIRepositoryStore(ctx, registryDomain, repositoryName, credsPolicy)
|
||||
},
|
||||
), nil
|
||||
|
||||
default:
|
||||
// We should not get here because the set of cases above should
|
||||
@@ -261,3 +277,77 @@ 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