github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/depsfile/locks.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package depsfile
     5  
     6  import (
     7  	"fmt"
     8  	"sort"
     9  
    10  	"github.com/terramate-io/tf/addrs"
    11  	"github.com/terramate-io/tf/getproviders"
    12  )
    13  
    14  // Locks is the top-level type representing the information retained in a
    15  // dependency lock file.
    16  //
    17  // Locks and the other types used within it are mutable via various setter
    18  // methods, but they are not safe for concurrent  modifications, so it's the
    19  // caller's responsibility to prevent concurrent writes and writes concurrent
    20  // with reads.
    21  type Locks struct {
    22  	providers map[addrs.Provider]*ProviderLock
    23  
    24  	// overriddenProviders is a subset of providers which we might be tracking
    25  	// in field providers but whose lock information we're disregarding for
    26  	// this particular run due to some feature that forces Terraform to not
    27  	// use a normally-installed plugin for it. For example, the "provider dev
    28  	// overrides" feature means that we'll be using an arbitrary directory on
    29  	// disk as the package, regardless of what might be selected in "providers".
    30  	//
    31  	// overriddenProviders is an in-memory-only annotation, never stored as
    32  	// part of a lock file and thus not persistent between Terraform runs.
    33  	// The CLI layer is generally the one responsible for populating this,
    34  	// by calling SetProviderOverridden in response to CLI Configuration
    35  	// settings, environment variables, or whatever similar sources.
    36  	overriddenProviders map[addrs.Provider]struct{}
    37  
    38  	// TODO: In future we'll also have module locks, but the design of that
    39  	// still needs some more work and we're deferring that to get the
    40  	// provider locking capability out sooner, because it's more common to
    41  	// directly depend on providers maintained outside your organization than
    42  	// modules maintained outside your organization.
    43  
    44  	// sources is a copy of the map of source buffers produced by the HCL
    45  	// parser during loading, which we retain only so that the caller can
    46  	// use it to produce source code snippets in error messages.
    47  	sources map[string][]byte
    48  }
    49  
    50  // NewLocks constructs and returns a new Locks object that initially contains
    51  // no locks at all.
    52  func NewLocks() *Locks {
    53  	return &Locks{
    54  		providers: make(map[addrs.Provider]*ProviderLock),
    55  
    56  		// no "sources" here, because that's only for locks objects loaded
    57  		// from files.
    58  	}
    59  }
    60  
    61  // Provider returns the stored lock for the given provider, or nil if that
    62  // provider currently has no lock.
    63  func (l *Locks) Provider(addr addrs.Provider) *ProviderLock {
    64  	return l.providers[addr]
    65  }
    66  
    67  // AllProviders returns a map describing all of the provider locks in the
    68  // receiver.
    69  func (l *Locks) AllProviders() map[addrs.Provider]*ProviderLock {
    70  	// We return a copy of our internal map so that future calls to
    71  	// SetProvider won't modify the map we're returning, or vice-versa.
    72  	ret := make(map[addrs.Provider]*ProviderLock, len(l.providers))
    73  	for k, v := range l.providers {
    74  		ret[k] = v
    75  	}
    76  	return ret
    77  }
    78  
    79  // SetProvider creates a new lock or replaces the existing lock for the given
    80  // provider.
    81  //
    82  // SetProvider returns the newly-created provider lock object, which
    83  // invalidates any ProviderLock object previously returned from Provider or
    84  // SetProvider for the given provider address.
    85  //
    86  // The ownership of the backing array for the slice of hashes passes to this
    87  // function, and so the caller must not read or write that backing array after
    88  // calling SetProvider.
    89  //
    90  // Only lockable providers can be passed to this method. If you pass a
    91  // non-lockable provider address then this function will panic. Use
    92  // function ProviderIsLockable to determine whether a particular provider
    93  // should participate in the version locking mechanism.
    94  func (l *Locks) SetProvider(addr addrs.Provider, version getproviders.Version, constraints getproviders.VersionConstraints, hashes []getproviders.Hash) *ProviderLock {
    95  	if !ProviderIsLockable(addr) {
    96  		panic(fmt.Sprintf("Locks.SetProvider with non-lockable provider %s", addr))
    97  	}
    98  
    99  	new := NewProviderLock(addr, version, constraints, hashes)
   100  	l.providers[new.addr] = new
   101  	return new
   102  }
   103  
   104  // RemoveProvider removes any existing lock file entry for the given provider.
   105  //
   106  // If the given provider did not already have a lock entry, RemoveProvider is
   107  // a no-op.
   108  //
   109  // Only lockable providers can be passed to this method. If you pass a
   110  // non-lockable provider address then this function will panic. Use
   111  // function ProviderIsLockable to determine whether a particular provider
   112  // should participate in the version locking mechanism.
   113  func (l *Locks) RemoveProvider(addr addrs.Provider) {
   114  	if !ProviderIsLockable(addr) {
   115  		panic(fmt.Sprintf("Locks.RemoveProvider with non-lockable provider %s", addr))
   116  	}
   117  
   118  	delete(l.providers, addr)
   119  }
   120  
   121  // SetProviderOverridden records that this particular Terraform process will
   122  // not pay attention to the recorded lock entry for the given provider, and
   123  // will instead access that provider's functionality in some other special
   124  // way that isn't sensitive to provider version selections or checksums.
   125  //
   126  // This is an in-memory-only annotation which lives only inside a particular
   127  // Locks object, and is never persisted as part of a saved lock file on disk.
   128  // It's valid to still use other methods of the reciever to access
   129  // already-stored lock information and to update lock information for an
   130  // overridden provider, but some callers may need to use ProviderIsOverridden
   131  // to selectively disregard stored lock information for overridden providers,
   132  // depending on what they intended to use the lock information for.
   133  func (l *Locks) SetProviderOverridden(addr addrs.Provider) {
   134  	if l.overriddenProviders == nil {
   135  		l.overriddenProviders = make(map[addrs.Provider]struct{})
   136  	}
   137  	l.overriddenProviders[addr] = struct{}{}
   138  }
   139  
   140  // ProviderIsOverridden returns true only if the given provider address was
   141  // previously registered as overridden by calling SetProviderOverridden.
   142  func (l *Locks) ProviderIsOverridden(addr addrs.Provider) bool {
   143  	_, ret := l.overriddenProviders[addr]
   144  	return ret
   145  }
   146  
   147  // SetSameOverriddenProviders updates the receiver to mark as overridden all
   148  // of the same providers already marked as overridden in the other given locks.
   149  //
   150  // This allows propagating override information between different lock objects,
   151  // as if calling SetProviderOverridden for each address already overridden
   152  // in the other given locks. If the reciever already has overridden providers,
   153  // SetSameOverriddenProviders will preserve them.
   154  func (l *Locks) SetSameOverriddenProviders(other *Locks) {
   155  	if other == nil {
   156  		return
   157  	}
   158  	for addr := range other.overriddenProviders {
   159  		l.SetProviderOverridden(addr)
   160  	}
   161  }
   162  
   163  // NewProviderLock creates a new ProviderLock object that isn't associated
   164  // with any Locks object.
   165  //
   166  // This is here primarily for testing. Most callers should use Locks.SetProvider
   167  // to construct a new provider lock and insert it into a Locks object at the
   168  // same time.
   169  //
   170  // The ownership of the backing array for the slice of hashes passes to this
   171  // function, and so the caller must not read or write that backing array after
   172  // calling NewProviderLock.
   173  //
   174  // Only lockable providers can be passed to this method. If you pass a
   175  // non-lockable provider address then this function will panic. Use
   176  // function ProviderIsLockable to determine whether a particular provider
   177  // should participate in the version locking mechanism.
   178  func NewProviderLock(addr addrs.Provider, version getproviders.Version, constraints getproviders.VersionConstraints, hashes []getproviders.Hash) *ProviderLock {
   179  	if !ProviderIsLockable(addr) {
   180  		panic(fmt.Sprintf("Locks.NewProviderLock with non-lockable provider %s", addr))
   181  	}
   182  
   183  	// Normalize the hashes into lexical order so that we can do straightforward
   184  	// equality tests between different locks for the same provider. The
   185  	// hashes are logically a set, so the given order is insignificant.
   186  	sort.Slice(hashes, func(i, j int) bool {
   187  		return string(hashes[i]) < string(hashes[j])
   188  	})
   189  
   190  	// This is a slightly-tricky in-place deduping to avoid unnecessarily
   191  	// allocating a new array in the common case where there are no duplicates:
   192  	// we iterate over "hashes" at the same time as appending to another slice
   193  	// with the same backing array, relying on the fact that deduping can only
   194  	// _skip_ elements from the input, and will never generate additional ones
   195  	// that would cause the writer to get ahead of the reader. This also
   196  	// assumes that we already sorted the items, which means that any duplicates
   197  	// will be consecutive in the sequence.
   198  	dedupeHashes := hashes[:0]
   199  	prevHash := getproviders.NilHash
   200  	for _, hash := range hashes {
   201  		if hash != prevHash {
   202  			dedupeHashes = append(dedupeHashes, hash)
   203  			prevHash = hash
   204  		}
   205  	}
   206  
   207  	return &ProviderLock{
   208  		addr:               addr,
   209  		version:            version,
   210  		versionConstraints: constraints,
   211  		hashes:             dedupeHashes,
   212  	}
   213  }
   214  
   215  // ProviderIsLockable returns true if the given provider is eligible for
   216  // version locking.
   217  //
   218  // Currently, all providers except builtin and legacy providers are eligible
   219  // for locking.
   220  func ProviderIsLockable(addr addrs.Provider) bool {
   221  	return !(addr.IsBuiltIn() || addr.IsLegacy())
   222  }
   223  
   224  // Sources returns the source code of the file the receiver was generated from,
   225  // or an empty map if the receiver wasn't generated from a file.
   226  //
   227  // This return type matches the one expected by HCL diagnostics printers to
   228  // produce source code snapshots, which is the only intended use for this
   229  // method.
   230  func (l *Locks) Sources() map[string][]byte {
   231  	return l.sources
   232  }
   233  
   234  // Equal returns true if the given Locks represents the same information as
   235  // the receiver.
   236  //
   237  // Equal explicitly _does not_ consider the equality of version constraints
   238  // in the saved locks, because those are saved only as hints to help the UI
   239  // explain what's changed between runs, and are never used as part of
   240  // dependency installation decisions.
   241  func (l *Locks) Equal(other *Locks) bool {
   242  	if len(l.providers) != len(other.providers) {
   243  		return false
   244  	}
   245  	for addr, thisLock := range l.providers {
   246  		otherLock, ok := other.providers[addr]
   247  		if !ok {
   248  			return false
   249  		}
   250  
   251  		if thisLock.addr != otherLock.addr {
   252  			// It'd be weird to get here because we already looked these up
   253  			// by address above.
   254  			return false
   255  		}
   256  		if thisLock.version != otherLock.version {
   257  			// Equality rather than "Version.Same" because changes to the
   258  			// build metadata are significant for the purpose of this function:
   259  			// it's a different package even if it has the same precedence.
   260  			return false
   261  		}
   262  
   263  		// Although "hashes" is declared as a slice, it's logically an
   264  		// unordered set. However, we normalize the slice of hashes when
   265  		// recieving it in NewProviderLock, so we can just do a simple
   266  		// item-by-item equality test here.
   267  		if len(thisLock.hashes) != len(otherLock.hashes) {
   268  			return false
   269  		}
   270  		for i := range thisLock.hashes {
   271  			if thisLock.hashes[i] != otherLock.hashes[i] {
   272  				return false
   273  			}
   274  		}
   275  	}
   276  	// We don't need to worry about providers that are in "other" but not
   277  	// in the receiver, because we tested the lengths being equal above.
   278  
   279  	return true
   280  }
   281  
   282  // EqualProviderAddress returns true if the given Locks have the same provider
   283  // address as the receiver. This doesn't check version and hashes.
   284  func (l *Locks) EqualProviderAddress(other *Locks) bool {
   285  	if len(l.providers) != len(other.providers) {
   286  		return false
   287  	}
   288  
   289  	for addr := range l.providers {
   290  		_, ok := other.providers[addr]
   291  		if !ok {
   292  			return false
   293  		}
   294  	}
   295  
   296  	return true
   297  }
   298  
   299  // Empty returns true if the given Locks object contains no actual locks.
   300  //
   301  // UI code might wish to use this to distinguish a lock file being
   302  // written for the first time from subsequent updates to that lock file.
   303  func (l *Locks) Empty() bool {
   304  	return len(l.providers) == 0
   305  }
   306  
   307  // DeepCopy creates a new Locks that represents the same information as the
   308  // receiver but does not share memory for any parts of the structure that.
   309  // are mutable through methods on Locks.
   310  //
   311  // Note that this does _not_ create deep copies of parts of the structure
   312  // that are technically mutable but are immutable by convention, such as the
   313  // array underlying the slice of version constraints. Callers may mutate the
   314  // resulting data structure only via the direct methods of Locks.
   315  func (l *Locks) DeepCopy() *Locks {
   316  	ret := NewLocks()
   317  	for addr, lock := range l.providers {
   318  		var hashes []getproviders.Hash
   319  		if len(lock.hashes) > 0 {
   320  			hashes = make([]getproviders.Hash, len(lock.hashes))
   321  			copy(hashes, lock.hashes)
   322  		}
   323  		ret.SetProvider(addr, lock.version, lock.versionConstraints, hashes)
   324  	}
   325  	return ret
   326  }
   327  
   328  // ProviderLock represents lock information for a specific provider.
   329  type ProviderLock struct {
   330  	// addr is the address of the provider this lock applies to.
   331  	addr addrs.Provider
   332  
   333  	// version is the specific version that was previously selected, while
   334  	// versionConstraints is the constraint that was used to make that
   335  	// selection, which we can potentially use to hint to run
   336  	// e.g. terraform init -upgrade if a user has changed a version
   337  	// constraint but the previous selection still remains valid.
   338  	// "version" is therefore authoritative, while "versionConstraints" is
   339  	// just for a UI hint and not used to make any real decisions.
   340  	version            getproviders.Version
   341  	versionConstraints getproviders.VersionConstraints
   342  
   343  	// hashes contains zero or more hashes of packages or package contents
   344  	// for the package associated with the selected version across all of
   345  	// the supported platforms.
   346  	//
   347  	// hashes can contain a mixture of hashes in different formats to support
   348  	// changes over time. The new-style hash format is to have a string
   349  	// starting with "h" followed by a version number and then a colon, like
   350  	// "h1:" for the first hash format version. Other hash versions following
   351  	// this scheme may come later. These versioned hash schemes are implemented
   352  	// in the getproviders package; for example, "h1:" is implemented in
   353  	// getproviders.HashV1 .
   354  	//
   355  	// There is also a legacy hash format which is just a lowercase-hex-encoded
   356  	// SHA256 hash of the official upstream .zip file for the selected version.
   357  	// We'll allow as that a stop-gap until we can upgrade Terraform Registry
   358  	// to support the new scheme, but is non-ideal because we can verify it only
   359  	// when we have the original .zip file exactly; we can't verify a local
   360  	// directory containing the unpacked contents of that .zip file.
   361  	//
   362  	// We ideally want to populate hashes for all available platforms at
   363  	// once, by referring to the signed checksums file in the upstream
   364  	// registry. In that ideal case it's possible to later work with the same
   365  	// configuration on a different platform while still verifying the hashes.
   366  	// However, installation from any method other than an origin registry
   367  	// means we can only populate the hash for the current platform, and so
   368  	// it won't be possible to verify a subsequent installation of the same
   369  	// provider on a different platform.
   370  	hashes []getproviders.Hash
   371  }
   372  
   373  // Provider returns the address of the provider this lock applies to.
   374  func (l *ProviderLock) Provider() addrs.Provider {
   375  	return l.addr
   376  }
   377  
   378  // Version returns the currently-selected version for the corresponding provider.
   379  func (l *ProviderLock) Version() getproviders.Version {
   380  	return l.version
   381  }
   382  
   383  // VersionConstraints returns the version constraints that were recorded as
   384  // being used to choose the version returned by Version.
   385  //
   386  // These version constraints are not authoritative for future selections and
   387  // are included only so Terraform can detect if the constraints in
   388  // configuration have changed since a selection was made, and thus hint to the
   389  // user that they may need to run terraform init -upgrade to apply the new
   390  // constraints.
   391  func (l *ProviderLock) VersionConstraints() getproviders.VersionConstraints {
   392  	return l.versionConstraints
   393  }
   394  
   395  // AllHashes returns all of the package hashes that were recorded when this
   396  // lock was created. If no hashes were recorded for that platform, the result
   397  // is a zero-length slice.
   398  //
   399  // If your intent is to verify a package against the recorded hashes, use
   400  // PreferredHashes to get only the hashes which the current version
   401  // of Terraform considers the strongest of the available hashing schemes, one
   402  // of which must match in order for verification to be considered successful.
   403  //
   404  // Do not modify the backing array of the returned slice.
   405  func (l *ProviderLock) AllHashes() []getproviders.Hash {
   406  	return l.hashes
   407  }
   408  
   409  // ContainsAll returns true if the hashes in this ProviderLock contains
   410  // all the hashes in the target.
   411  //
   412  // This function assumes the hashes are in each ProviderLock are sorted.
   413  // If the ProviderLock was created by the NewProviderLock constructor then
   414  // the hashes are guaranteed to be sorted.
   415  func (l *ProviderLock) ContainsAll(target *ProviderLock) bool {
   416  	if target == nil || len(target.hashes) == 0 {
   417  		return true
   418  	}
   419  
   420  	targetIndex := 0
   421  	for ix := 0; ix < len(l.hashes); ix++ {
   422  		if l.hashes[ix] == target.hashes[targetIndex] {
   423  			targetIndex++
   424  
   425  			if targetIndex >= len(target.hashes) {
   426  				return true
   427  			}
   428  		}
   429  	}
   430  	return false
   431  }
   432  
   433  // PreferredHashes returns a filtered version of the AllHashes return value
   434  // which includes only the strongest of the availabile hash schemes, in
   435  // case legacy hash schemes are deprecated over time but still supported for
   436  // upgrade purposes.
   437  //
   438  // At least one of the given hashes must match for a package to be considered
   439  // valud.
   440  func (l *ProviderLock) PreferredHashes() []getproviders.Hash {
   441  	return getproviders.PreferredHashes(l.hashes)
   442  }