Files
opentf/internal/depsfile/locks.go
Martin Atkins d2bef1fd47 Adopt OpenTofu's own "svchost" module
Previously we were using a third-party library, but that doesn't have any
support for passing context.Context through its API and so isn't suitable
for our goals of adding OpenTelemetry tracing for all outgoing network
requests.

We now have our own fork that is updated to use context.Context. It also
has a slightly reduced scope no longer including various details that
are tightly-coupled to our cliconfig mechanism and so better placed in the
main OpenTofu codebase so we can evolve it in future without making
lockstep library releases.

The "registry-address" library also uses svchost and uses some of its types
in its public API, so this also incorporates v2 of that library that is
updated to use our own svchost module.

Unfortunately this commit is a mix of mechanical updates to the new
libraries and some new code dealing with the functionality that is removed
in our fork of svchost. The new code is primarily in the "svcauthconfig"
package, which is similar in purpose "ociauthconfig" but for OpenTofu's
own auth mechanism instead of the OCI Distribution protocol's auth
mechanism.

This includes some additional plumbing of context.Context where it was
possible to do so without broad changes to files that would not otherwise
have been included in this commit, but there are a few leftover spots that
are context.TODO() which we'll address separately in later commits.

This removes the temporary workaround from d079da6e9e, since we are now
able to plumb the OpenTelemetry span tree all the way to the service
discovery requests.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-06-12 09:37:59 -07:00

500 lines
20 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 depsfile
import (
"fmt"
"sort"
svchost "github.com/opentofu/svchost"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/getproviders"
)
// Locks is the top-level type representing the information retained in a
// dependency lock file.
//
// Locks and the other types used within it are mutable via various setter
// methods, but they are not safe for concurrent modifications, so it's the
// caller's responsibility to prevent concurrent writes and writes concurrent
// with reads.
type Locks struct {
providers map[addrs.Provider]*ProviderLock
// overriddenProviders is a subset of providers which we might be tracking
// in field providers but whose lock information we're disregarding for
// this particular run due to some feature that forces OpenTofu to not
// use a normally-installed plugin for it. For example, the "provider dev
// overrides" feature means that we'll be using an arbitrary directory on
// disk as the package, regardless of what might be selected in "providers".
//
// overriddenProviders is an in-memory-only annotation, never stored as
// part of a lock file and thus not persistent between OpenTofu runs.
// The CLI layer is generally the one responsible for populating this,
// by calling SetProviderOverridden in response to CLI Configuration
// settings, environment variables, or whatever similar sources.
overriddenProviders map[addrs.Provider]struct{}
// TODO: In future we'll also have module locks, but the design of that
// still needs some more work and we're deferring that to get the
// provider locking capability out sooner, because it's more common to
// directly depend on providers maintained outside your organization than
// modules maintained outside your organization.
// sources is a copy of the map of source buffers produced by the HCL
// parser during loading, which we retain only so that the caller can
// use it to produce source code snippets in error messages.
sources map[string][]byte
}
// NewLocks constructs and returns a new Locks object that initially contains
// no locks at all.
func NewLocks() *Locks {
return &Locks{
providers: make(map[addrs.Provider]*ProviderLock),
// no "sources" here, because that's only for locks objects loaded
// from files.
}
}
// Provider returns the stored lock for the given provider, or nil if that
// provider currently has no lock.
func (l *Locks) Provider(addr addrs.Provider) *ProviderLock {
return l.providers[addr]
}
// AllProviders returns a map describing all of the provider locks in the
// receiver.
func (l *Locks) AllProviders() map[addrs.Provider]*ProviderLock {
// We return a copy of our internal map so that future calls to
// SetProvider won't modify the map we're returning, or vice-versa.
ret := make(map[addrs.Provider]*ProviderLock, len(l.providers))
for k, v := range l.providers {
ret[k] = v
}
return ret
}
// SetProvider creates a new lock or replaces the existing lock for the given
// provider.
//
// SetProvider returns the newly-created provider lock object, which
// invalidates any ProviderLock object previously returned from Provider or
// SetProvider for the given provider address.
//
// The ownership of the backing array for the slice of hashes passes to this
// function, and so the caller must not read or write that backing array after
// calling SetProvider.
//
// Only lockable providers can be passed to this method. If you pass a
// non-lockable provider address then this function will panic. Use
// function ProviderIsLockable to determine whether a particular provider
// should participate in the version locking mechanism.
func (l *Locks) SetProvider(addr addrs.Provider, version getproviders.Version, constraints getproviders.VersionConstraints, hashes []getproviders.Hash) *ProviderLock {
if !ProviderIsLockable(addr) {
panic(fmt.Sprintf("Locks.SetProvider with non-lockable provider %s", addr))
}
new := NewProviderLock(addr, version, constraints, hashes)
l.providers[new.addr] = new
return new
}
// RemoveProvider removes any existing lock file entry for the given provider.
//
// If the given provider did not already have a lock entry, RemoveProvider is
// a no-op.
//
// Only lockable providers can be passed to this method. If you pass a
// non-lockable provider address then this function will panic. Use
// function ProviderIsLockable to determine whether a particular provider
// should participate in the version locking mechanism.
func (l *Locks) RemoveProvider(addr addrs.Provider) {
if !ProviderIsLockable(addr) {
panic(fmt.Sprintf("Locks.RemoveProvider with non-lockable provider %s", addr))
}
delete(l.providers, addr)
}
// SetProviderOverridden records that this particular OpenTofu process will
// not pay attention to the recorded lock entry for the given provider, and
// will instead access that provider's functionality in some other special
// way that isn't sensitive to provider version selections or checksums.
//
// This is an in-memory-only annotation which lives only inside a particular
// Locks object, and is never persisted as part of a saved lock file on disk.
// It's valid to still use other methods of the receiver to access
// already-stored lock information and to update lock information for an
// overridden provider, but some callers may need to use ProviderIsOverridden
// to selectively disregard stored lock information for overridden providers,
// depending on what they intended to use the lock information for.
func (l *Locks) SetProviderOverridden(addr addrs.Provider) {
if l.overriddenProviders == nil {
l.overriddenProviders = make(map[addrs.Provider]struct{})
}
l.overriddenProviders[addr] = struct{}{}
}
// ProviderIsOverridden returns true only if the given provider address was
// previously registered as overridden by calling SetProviderOverridden.
func (l *Locks) ProviderIsOverridden(addr addrs.Provider) bool {
_, ret := l.overriddenProviders[addr]
return ret
}
// SetSameOverriddenProviders updates the receiver to mark as overridden all
// of the same providers already marked as overridden in the other given locks.
//
// This allows propagating override information between different lock objects,
// as if calling SetProviderOverridden for each address already overridden
// in the other given locks. If the receiver already has overridden providers,
// SetSameOverriddenProviders will preserve them.
func (l *Locks) SetSameOverriddenProviders(other *Locks) {
if other == nil {
return
}
for addr := range other.overriddenProviders {
l.SetProviderOverridden(addr)
}
}
// NewProviderLock creates a new ProviderLock object that isn't associated
// with any Locks object.
//
// This is here primarily for testing. Most callers should use Locks.SetProvider
// to construct a new provider lock and insert it into a Locks object at the
// same time.
//
// The ownership of the backing array for the slice of hashes passes to this
// function, and so the caller must not read or write that backing array after
// calling NewProviderLock.
//
// Only lockable providers can be passed to this method. If you pass a
// non-lockable provider address then this function will panic. Use
// function ProviderIsLockable to determine whether a particular provider
// should participate in the version locking mechanism.
func NewProviderLock(addr addrs.Provider, version getproviders.Version, constraints getproviders.VersionConstraints, hashes []getproviders.Hash) *ProviderLock {
if !ProviderIsLockable(addr) {
panic(fmt.Sprintf("Locks.NewProviderLock with non-lockable provider %s", addr))
}
// Normalize the hashes into lexical order so that we can do straightforward
// equality tests between different locks for the same provider. The
// hashes are logically a set, so the given order is insignificant.
sort.Slice(hashes, func(i, j int) bool {
return string(hashes[i]) < string(hashes[j])
})
// This is a slightly-tricky in-place deduping to avoid unnecessarily
// allocating a new array in the common case where there are no duplicates:
// we iterate over "hashes" at the same time as appending to another slice
// with the same backing array, relying on the fact that deduping can only
// _skip_ elements from the input, and will never generate additional ones
// that would cause the writer to get ahead of the reader. This also
// assumes that we already sorted the items, which means that any duplicates
// will be consecutive in the sequence.
dedupeHashes := hashes[:0]
prevHash := getproviders.NilHash
for _, hash := range hashes {
if hash != prevHash {
dedupeHashes = append(dedupeHashes, hash)
prevHash = hash
}
}
return &ProviderLock{
addr: addr,
version: version,
versionConstraints: constraints,
hashes: dedupeHashes,
}
}
// ProviderIsLockable returns true if the given provider is eligible for
// version locking.
//
// Currently, all providers except builtin and legacy providers are eligible
// for locking.
func ProviderIsLockable(addr addrs.Provider) bool {
return !addr.IsBuiltIn() && !addr.IsLegacy()
}
// Sources returns the source code of the file the receiver was generated from,
// or an empty map if the receiver wasn't generated from a file.
//
// This return type matches the one expected by HCL diagnostics printers to
// produce source code snapshots, which is the only intended use for this
// method.
func (l *Locks) Sources() map[string][]byte {
return l.sources
}
// Equal returns true if the given Locks represents the same information as
// the receiver.
//
// Equal explicitly _does not_ consider the equality of version constraints
// in the saved locks, because those are saved only as hints to help the UI
// explain what's changed between runs, and are never used as part of
// dependency installation decisions.
func (l *Locks) Equal(other *Locks) bool {
if len(l.providers) != len(other.providers) {
return false
}
for addr, thisLock := range l.providers {
otherLock, ok := other.providers[addr]
if !ok {
return false
}
if thisLock.addr != otherLock.addr {
// It'd be weird to get here because we already looked these up
// by address above.
return false
}
if thisLock.version != otherLock.version {
// Equality rather than "Version.Same" because changes to the
// build metadata are significant for the purpose of this function:
// it's a different package even if it has the same precedence.
return false
}
// Although "hashes" is declared as a slice, it's logically an
// unordered set. However, we normalize the slice of hashes when
// receiving it in NewProviderLock, so we can just do a simple
// item-by-item equality test here.
if len(thisLock.hashes) != len(otherLock.hashes) {
return false
}
for i := range thisLock.hashes {
if thisLock.hashes[i] != otherLock.hashes[i] {
return false
}
}
}
// We don't need to worry about providers that are in "other" but not
// in the receiver, because we tested the lengths being equal above.
return true
}
// EqualProviderAddress returns true if the given Locks have the same provider
// address as the receiver. This doesn't check version and hashes.
func (l *Locks) EqualProviderAddress(other *Locks) bool {
if len(l.providers) != len(other.providers) {
return false
}
for addr := range l.providers {
_, ok := other.providers[addr]
if !ok {
return false
}
}
return true
}
// UpgradeFromPredecessorProject checks the stored locks for any that seem
// like they were probably created by the project that OpenTofu forked from,
// and if so adds similar lock entries for same-named providers in the OpenTofu
// registry.
//
// Because there is no guarantee that hashes would match between providers in
// the two registries, the replacement lock file entries initially have no
// hashes at all, and so can be used to select the same version number but
// not to guarantee that the provider package matches what was previously
// installed. The lock entries for the "old" providers are retained by this
// function, but are likely to be discarded if the locks are subsequently used
// for provider installation, since that discards lock entries for any providers
// not currently in use.
//
// The result is a map from the providers whose entries were replaced to the
// new provider that was selected to replace each one, so that the caller can
// explain the changes to the operator. The result is zero-length if no
// changes were made.
func (l *Locks) UpgradeFromPredecessorProject() map[addrs.Provider]addrs.Provider {
var ret map[addrs.Provider]addrs.Provider // initialized only if we change something
for oldAddr, oldLock := range l.providers {
if oldAddr.Hostname != svchost.Hostname("registry.terraform.io") || oldAddr.Namespace != "hashicorp" {
// Only providers in this namespace have corresponding providers
// in the OpenTofu registry that are controlled by the OpenTofu
// project. We cannot safely make any assumption of equivalence
// about providers in any other registry or namespace.
continue
}
newAddr := addrs.NewDefaultProvider(oldAddr.Type)
if _, ok := l.providers[newAddr]; ok {
continue // we already have an entry for the replacement provider, so we'll keep it
}
// Our new lock file entry retains the same version selection and
// version constraints but discards the hashes.
newLock := NewProviderLock(newAddr, oldLock.Version(), oldLock.VersionConstraints(), nil)
l.providers[newAddr] = newLock
// NOTE: We intentionally keep the old entry in here for now, and
// let subsequent provider installer work clean it up, because the
// provider installer can tell whether anything in the configuration
// or state is somehow still referring to the old provider, in which
// case we ought to keep the entry for it.
if ret == nil {
ret = make(map[addrs.Provider]addrs.Provider)
}
ret[oldAddr] = newAddr
}
return ret
}
// Empty returns true if the given Locks object contains no actual locks.
//
// UI code might wish to use this to distinguish a lock file being
// written for the first time from subsequent updates to that lock file.
func (l *Locks) Empty() bool {
return len(l.providers) == 0
}
// DeepCopy creates a new Locks that represents the same information as the
// receiver but does not share memory for any parts of the structure that.
// are mutable through methods on Locks.
//
// Note that this does _not_ create deep copies of parts of the structure
// that are technically mutable but are immutable by convention, such as the
// array underlying the slice of version constraints. Callers may mutate the
// resulting data structure only via the direct methods of Locks.
func (l *Locks) DeepCopy() *Locks {
ret := NewLocks()
for addr, lock := range l.providers {
var hashes []getproviders.Hash
if len(lock.hashes) > 0 {
hashes = make([]getproviders.Hash, len(lock.hashes))
copy(hashes, lock.hashes)
}
ret.SetProvider(addr, lock.version, lock.versionConstraints, hashes)
}
return ret
}
// ProviderLock represents lock information for a specific provider.
type ProviderLock struct {
// addr is the address of the provider this lock applies to.
addr addrs.Provider
// version is the specific version that was previously selected, while
// versionConstraints is the constraint that was used to make that
// selection, which we can potentially use to hint to run
// e.g. tofu init -upgrade if a user has changed a version
// constraint but the previous selection still remains valid.
// "version" is therefore authoritative, while "versionConstraints" is
// just for a UI hint and not used to make any real decisions.
version getproviders.Version
versionConstraints getproviders.VersionConstraints
// hashes contains zero or more hashes of packages or package contents
// for the package associated with the selected version across all of
// the supported platforms.
//
// hashes can contain a mixture of hashes in different formats to support
// changes over time. The new-style hash format is to have a string
// starting with "h" followed by a version number and then a colon, like
// "h1:" for the first hash format version. Other hash versions following
// this scheme may come later. These versioned hash schemes are implemented
// in the getproviders package; for example, "h1:" is implemented in
// getproviders.HashV1 .
//
// There is also a legacy hash format which is just a lowercase-hex-encoded
// SHA256 hash of the official upstream .zip file for the selected version.
// We'll allow as that a stop-gap until we can upgrade Terraform Registry
// to support the new scheme, but is non-ideal because we can verify it only
// when we have the original .zip file exactly; we can't verify a local
// directory containing the unpacked contents of that .zip file.
//
// We ideally want to populate hashes for all available platforms at
// once, by referring to the signed checksums file in the upstream
// registry. In that ideal case it's possible to later work with the same
// configuration on a different platform while still verifying the hashes.
// However, installation from any method other than an origin registry
// means we can only populate the hash for the current platform, and so
// it won't be possible to verify a subsequent installation of the same
// provider on a different platform.
hashes []getproviders.Hash
}
// Provider returns the address of the provider this lock applies to.
func (l *ProviderLock) Provider() addrs.Provider {
return l.addr
}
// Version returns the currently-selected version for the corresponding provider.
func (l *ProviderLock) Version() getproviders.Version {
return l.version
}
// VersionConstraints returns the version constraints that were recorded as
// being used to choose the version returned by Version.
//
// These version constraints are not authoritative for future selections and
// are included only so OpenTofu can detect if the constraints in
// configuration have changed since a selection was made, and thus hint to the
// user that they may need to run tofu init -upgrade to apply the new
// constraints.
func (l *ProviderLock) VersionConstraints() getproviders.VersionConstraints {
return l.versionConstraints
}
// AllHashes returns all of the package hashes that were recorded when this
// lock was created. If no hashes were recorded for that platform, the result
// is a zero-length slice.
//
// If your intent is to verify a package against the recorded hashes, use
// PreferredHashes to get only the hashes which the current version
// of OpenTofu considers the strongest of the available hashing schemes, one
// of which must match in order for verification to be considered successful.
//
// Do not modify the backing array of the returned slice.
func (l *ProviderLock) AllHashes() []getproviders.Hash {
return l.hashes
}
// ContainsAll returns true if the hashes in this ProviderLock contains
// all the hashes in the target.
//
// This function assumes the hashes are in each ProviderLock are sorted.
// If the ProviderLock was created by the NewProviderLock constructor then
// the hashes are guaranteed to be sorted.
func (l *ProviderLock) ContainsAll(target *ProviderLock) bool {
if target == nil || len(target.hashes) == 0 {
return true
}
targetIndex := 0
for ix := 0; ix < len(l.hashes); ix++ {
if l.hashes[ix] == target.hashes[targetIndex] {
targetIndex++
if targetIndex >= len(target.hashes) {
return true
}
}
}
return false
}
// PreferredHashes returns a filtered version of the AllHashes return value
// which includes only the strongest of the available hash schemes, in
// case legacy hash schemes are deprecated over time but still supported for
// upgrade purposes.
//
// At least one of the given hashes must match for a package to be considered
// valid.
func (l *ProviderLock) PreferredHashes() []getproviders.Hash {
return getproviders.PreferredHashes(l.hashes)
}