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 }