-
Notifications
You must be signed in to change notification settings - Fork 10
/
locks.go
439 lines (396 loc) · 17.1 KB
/
locks.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
package depsfile
import (
"fmt"
"sort"
"github.com/snyk/policy-engine/pkg/internal/terraform/addrs"
"github.com/snyk/policy-engine/pkg/internal/terraform/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 Terraform 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 Terraform 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 Terraform 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 reciever 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 reciever 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
// recieving 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
}
// 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. terraform 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 Terraform 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 terraform 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 Terraform 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 availabile 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
// valud.
func (l *ProviderLock) PreferredHashes() []getproviders.Hash {
return getproviders.PreferredHashes(l.hashes)
}