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