mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
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>
163 lines
6.7 KiB
Go
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
|
|
}
|