Files
opentf/internal/command/meta_dependencies.go
Martin Atkins b7e0a93708 command: Only shim dependency lock file for installation actions
Recently we added a call to Locks.UpgradeFromPredecessorProject to try to
preserve dependency selections made for providers under
registry.terraform.io/hashicorp/* when switching to OpenTofu for the first
time.

However, this behavior did not properly cater for the situation where the
configuration intentionally specifies registry.terraform.io explicitly in
a source address: that would then cause OpenTofu to incorrectly try to make
a factory function for the shimmed provider version when working in
command.Meta.providerFactories, which would then fail because no such
provider appears in the cache directory.

Instead then, we'll limit the shimming only to installation-related actions
while only using the dependency locks exactly as written when preparing to
actually _run_ the provider plugins.

This is bothersome to test because our tests are not allowed to directly
access registry.terraform.io; the test case here mimicks one case in which
it could be valid for an OpenTofu user to explicitly use
registry.terraform.io: if they've used the CLI configuration to arrange for
that hostname to be handled only via a mirror rather than by direct access
to the origin registry. The terms of service for registry.terraform.io
currently prohibit using it for anything other than Terraform, so we ensure
that this test cannot make requests to any real services at that hostname.

Note that telling OpenTofu to use registry.terraform.io is not officially
supported and may cause other problems beyond what was addressed by this
PR, since OpenTofu tends to assume that this hostname would appear only
during the process of migrating from Terraform and might make unexpected
decisions based on that assumption. Despite us making this fix, those who
are explicitly specifying registry.terraform.io in their configuration
should make plans to stop doing that and to set things up some other way
instead.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-07-01 07:44:47 -07:00

163 lines
6.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 command
import (
"context"
"fmt"
"log"
"maps"
"os"
"slices"
"strings"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// dependencyLockFilename is the filename of the dependency lock file.
//
// This file should live in the same directory as the .tf files for the
// root module of the configuration, alongside the .terraform directory
// as long as that directory's path isn't overridden by the TF_DATA_DIR
// environment variable.
//
// We always expect to find this file in the current working directory
// because that should also be the root module directory.
//
// Some commands have legacy command line arguments that make the root module
// directory something other than the root module directory; when using those,
// the lock file will be written in the "wrong" place (the current working
// directory instead of the root module directory) but we do that intentionally
// to match where the ".terraform" directory would also be written in that
// case. Eventually we will phase out those legacy arguments in favor of the
// global -chdir=... option, which _does_ preserve the intended invariant
// that the root module directory is always the current working directory.
const dependencyLockFilename = ".terraform.lock.hcl"
// lockedDependencies reads the dependency lock information from the lock file
// in the current working directory.
//
// If the lock file doesn't exist at the time of the call, lockedDependencies
// indicates success and returns an empty Locks object. If the file does
// exist then the result is either a representation of the contents of that
// file at the instant of the call or error diagnostics explaining some way
// in which the lock file is invalid.
//
// The result is a snapshot of the locked dependencies at the time of the call
// and does not update as a result of calling replaceLockedDependencies
// or any other modification method.
func (m *Meta) lockedDependencies() (*depsfile.Locks, tfdiags.Diagnostics) {
// We check that the file exists first, because the underlying HCL
// parser doesn't distinguish that error from other error types
// in a machine-readable way but we want to treat that as a success
// with no locks. There is in theory a race condition here in that
// the file could be created or removed in the meantime, but we're not
// promising to support two concurrent dependency installation processes.
_, err := os.Stat(dependencyLockFilename)
if os.IsNotExist(err) {
return m.annotateDependencyLocksWithOverrides(depsfile.NewLocks()), nil
}
ret, diags := depsfile.LoadLocksFromFile(dependencyLockFilename)
return m.annotateDependencyLocksWithOverrides(ret), diags
}
// lockedDependenciesWithPredecessorRegistryShimmed is a wrapper around
// [Meta.lockedDependencies] that adds some extra synthetic entries for any
// existing lock entry that matches "registry.terraform.io/hashicorp/*", to
// encourage the provider installer to select the same version of the
// corresponding provider in OpenTofu's registry, to keep dependency selections
// consistent as folks migrate over from our predecessor.
//
// This variant should be used only by commands that will perform provider
// installation based on the result, such as the implementation "tofu init".
// This is not appropriate to use when the result will be used to call
// [Meta.providerFactories]; that function needs to be given exactly the
// dependencies from the lock file, because it expects to find every listed
// provider in the cache directory and will fail if not.
func (m *Meta) lockedDependenciesWithPredecessorRegistryShimmed() (*depsfile.Locks, tfdiags.Diagnostics) {
ret, diags := m.lockedDependencies()
if ret == nil {
return nil, diags
}
// If this is the first run after switching from OpenTofu's predecessor,
// the lock file might contain some entries from the predecessor's registry
// which we can translate into similar entries for OpenTofu's registry.
changed := ret.UpgradeFromPredecessorProject()
if len(changed) != 0 {
oldAddrs := slices.Collect(maps.Keys(changed))
slices.SortFunc(oldAddrs, func(a, b addrs.Provider) int {
if a.LessThan(b) {
return -1
} else if b.LessThan(a) {
return 1
} else {
return 0
}
})
var buf strings.Builder // strings.Builder writes cannot fail
_, _ = buf.WriteString("OpenTofu automatically rewrote some entries in your dependency lock file:\n")
for _, oldAddr := range oldAddrs {
newAddr := changed[oldAddr]
// We intentionally use String instead of ForDisplay here because
// this message won't make much sense without using fully-qualified
// addresses with explicit registry hostnames.
_, _ = fmt.Fprintf(&buf, " - %s => %s\n", oldAddr.String(), newAddr.String())
}
_, _ = buf.WriteString("\nThe version selections were preserved, but the hashes were not because the OpenTofu project's provider releases are not byte-for-byte identical.")
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Dependency lock file entries automatically updated",
buf.String(),
))
// The newly-added entries might also be subject to one of the various
// kinds of "overrides" we support.
ret = m.annotateDependencyLocksWithOverrides(ret)
}
return ret, diags
}
// replaceLockedDependencies creates or overwrites the lock file in the
// current working directory to contain the information recorded in the given
// locks object.
func (m *Meta) replaceLockedDependencies(ctx context.Context, new *depsfile.Locks) tfdiags.Diagnostics {
return depsfile.SaveLocksToFile(ctx, new, dependencyLockFilename)
}
// annotateDependencyLocksWithOverrides modifies the given Locks object in-place
// to track as overridden any provider address that's subject to testing
// overrides, development overrides, or "unmanaged provider" status.
//
// This is just an implementation detail of the lockedDependencies method,
// not intended for use anywhere else.
func (m *Meta) annotateDependencyLocksWithOverrides(ret *depsfile.Locks) *depsfile.Locks {
if ret == nil {
return ret
}
for addr := range m.ProviderDevOverrides {
log.Printf("[DEBUG] Provider %s is overridden by dev_overrides", addr)
ret.SetProviderOverridden(addr)
}
for addr := range m.UnmanagedProviders {
log.Printf("[DEBUG] Provider %s is overridden as an \"unmanaged provider\"", addr)
ret.SetProviderOverridden(addr)
}
if m.testingOverrides != nil {
for addr := range m.testingOverrides.Providers {
log.Printf("[DEBUG] Provider %s is overridden in Meta.testingOverrides", addr)
ret.SetProviderOverridden(addr)
}
}
return ret
}