Skip to content

Commit 1fca13b

Browse files
providercache: Ignore lock-mismatching global cache entries
When we originally introduced the trust-on-first-use checksum locking mechanism in v0.14, we had to make some tricky decisions about how it should interact with the pre-existing optional read-through global cache of provider packages: The global cache essentially conflicts with the checksum locking because if the needed provider is already in the cache then Terraform skips installing the provider from upstream and therefore misses the opportunity to capture the signed checksums published by the provider developer. We can't use the signed checksums to verify a cache entry because the origin registry protocol is still using the legacy ziphash scheme and that is only usable for the original zipped provider packages and not for the unpacked-layout cache directory. Therefore we decided to prioritize the existing cache directory behavior at the expense of the lock file behavior, making Terraform produce an incomplete lock file in that case. Now that we've had some real-world experience with the lock file mechanism, we can see that the chosen compromise was not ideal because it causes "terraform init" to behave significantly differently in its lock file update behavior depending on whether or not a particular provider is already cached. By robbing Terraform of its opportunity to fetch the official checksums, Terraform must generate a lock file that is inherently non-portable, which is problematic for any team which works with the same Terraform configuration on multiple different platforms. This change addresses that problem by essentially flipping the decision so that we'll prioritize the lock file behavior over the provider cache behavior. Now a global cache entry is eligible for use if and only if the lock file already contains a checksum that matches the cache entry. This means that the first time a particular configuration sees a new provider it will always be fetched from the configured installation source (typically the origin registry) and record the checksums from that source. On subsequent installs of the same provider version already locked, Terraform will then consider the cache entry to be eligible and skip re-downloading the same package. This intentionally makes the global cache mechanism subordinate to the lock file mechanism: the lock file must be populated in order for the global cache to be effective. For those who have many separate configurations which all refer to the same provider version, they will need to re-download the provider once for each configuration in order to gather the information needed to populate the lock file, whereas before they would have only downloaded it for the _first_ configuration using that provider. This should therefore remove the most significant cause of folks ending up with incomplete lock files that don't work for colleagues using other platforms, and the expense of bypassing the cache for the first use of each new package with each new configuration. This tradeoff seems reasonable because otherwise such users would inevitably need to run "terraform providers lock" separately anyway, and that command _always_ bypasses the cache. Although this change does decrease the hit rate of the cache, if we subtract the never-cached downloads caused by "terraform providers lock" then this is a net benefit overall, and does the right thing by default without the need to run a separate command.
1 parent ff68c8d commit 1fca13b

File tree

4 files changed

+501
-91
lines changed

4 files changed

+501
-91
lines changed

internal/providercache/installer.go

Lines changed: 142 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -347,98 +347,134 @@ NeedProvider:
347347
// Step 3a: If our global cache already has this version available then
348348
// we'll just link it in.
349349
if cached := i.globalCacheDir.ProviderVersion(provider, version); cached != nil {
350-
if cb := evts.LinkFromCacheBegin; cb != nil {
351-
cb(provider, version, i.globalCacheDir.baseDir)
352-
}
353-
if _, err := cached.ExecutableFile(); err != nil {
354-
err := fmt.Errorf("provider binary not found: %s", err)
355-
errs[provider] = err
356-
if cb := evts.LinkFromCacheFailure; cb != nil {
357-
cb(provider, version, err)
350+
// An existing cache entry is only an acceptable choice
351+
// if there is already a lock file entry for this provider
352+
// and the cache entry matches its checksums.
353+
//
354+
// If there was no lock file entry at all then we need to
355+
// install the package for real so that we can lock as complete
356+
// as possible a set of checksums for all of this provider's
357+
// packages.
358+
//
359+
// If there was a lock file entry but the cache doesn't match
360+
// it then we assume that the lock file checksums were only
361+
// partially populated (e.g. from a local mirror where we can
362+
// only see one package to checksum it) and so we'll fetch
363+
// from upstream to see if the origin can give us a package
364+
// that _does_ match. This might still not work out, but if
365+
// it does then it allows us to avoid returning a checksum
366+
// mismatch error.
367+
acceptablePackage := false
368+
if len(preferredHashes) != 0 {
369+
var err error
370+
acceptablePackage, err = cached.MatchesAnyHash(preferredHashes)
371+
if err != nil {
372+
// If we can't calculate the checksum for the cached
373+
// package then we'll just treat it as a checksum failure.
374+
acceptablePackage = false
358375
}
359-
continue
360376
}
361377

362-
err := i.targetDir.LinkFromOtherCache(cached, preferredHashes)
363-
if err != nil {
364-
errs[provider] = err
365-
if cb := evts.LinkFromCacheFailure; cb != nil {
366-
cb(provider, version, err)
378+
// TODO: Should we emit an event through the events object
379+
// for "there was an entry in the cache but we ignored it
380+
// because the checksum didn't match"? We can't use
381+
// LinkFromCacheFailure in that case because this isn't a
382+
// failure. For now we'll just be quiet about it.
383+
384+
if acceptablePackage {
385+
if cb := evts.LinkFromCacheBegin; cb != nil {
386+
cb(provider, version, i.globalCacheDir.baseDir)
367387
}
368-
continue
369-
}
370-
// We'll fetch what we just linked to make sure it actually
371-
// did show up there.
372-
new := i.targetDir.ProviderVersion(provider, version)
373-
if new == nil {
374-
err := fmt.Errorf("after linking %s from provider cache at %s it is still not detected in the target directory; this is a bug in Terraform", provider, i.globalCacheDir.baseDir)
375-
errs[provider] = err
376-
if cb := evts.LinkFromCacheFailure; cb != nil {
377-
cb(provider, version, err)
388+
if _, err := cached.ExecutableFile(); err != nil {
389+
err := fmt.Errorf("provider binary not found: %s", err)
390+
errs[provider] = err
391+
if cb := evts.LinkFromCacheFailure; cb != nil {
392+
cb(provider, version, err)
393+
}
394+
continue
378395
}
379-
continue
380-
}
381396

382-
// The LinkFromOtherCache call above should've verified that
383-
// the package matches one of the hashes previously recorded,
384-
// if any. We'll now augment those hashes with one freshly
385-
// calculated from the package we just linked, which allows
386-
// the lock file to gradually transition to recording newer hash
387-
// schemes when they become available.
388-
var priorHashes []getproviders.Hash
389-
if lock != nil && lock.Version() == version {
390-
// If the version we're installing is identical to the
391-
// one we previously locked then we'll keep all of the
392-
// hashes we saved previously and add to it. Otherwise
393-
// we'll be starting fresh, because each version has its
394-
// own set of packages and thus its own hashes.
395-
priorHashes = append(priorHashes, preferredHashes...)
396-
397-
// NOTE: The behavior here is unfortunate when a particular
398-
// provider version was already cached on the first time
399-
// the current configuration requested it, because that
400-
// means we don't currently get the opportunity to fetch
401-
// and verify the checksums for the new package from
402-
// upstream. That's currently unavoidable because upstream
403-
// checksums are in the "ziphash" format and so we can't
404-
// verify them against our cache directory's unpacked
405-
// packages: we'd need to go fetch the package from the
406-
// origin and compare against it, which would defeat the
407-
// purpose of the global cache.
408-
//
409-
// If we fetch from upstream on the first encounter with
410-
// a particular provider then we'll end up in the other
411-
// codepath below where we're able to also include the
412-
// checksums from the origin registry.
413-
}
414-
newHash, err := cached.Hash()
415-
if err != nil {
416-
err := fmt.Errorf("after linking %s from provider cache at %s, failed to compute a checksum for it: %s", provider, i.globalCacheDir.baseDir, err)
417-
errs[provider] = err
418-
if cb := evts.LinkFromCacheFailure; cb != nil {
419-
cb(provider, version, err)
397+
err := i.targetDir.LinkFromOtherCache(cached, preferredHashes)
398+
if err != nil {
399+
errs[provider] = err
400+
if cb := evts.LinkFromCacheFailure; cb != nil {
401+
cb(provider, version, err)
402+
}
403+
continue
404+
}
405+
// We'll fetch what we just linked to make sure it actually
406+
// did show up there.
407+
new := i.targetDir.ProviderVersion(provider, version)
408+
if new == nil {
409+
err := fmt.Errorf("after linking %s from provider cache at %s it is still not detected in the target directory; this is a bug in Terraform", provider, i.globalCacheDir.baseDir)
410+
errs[provider] = err
411+
if cb := evts.LinkFromCacheFailure; cb != nil {
412+
cb(provider, version, err)
413+
}
414+
continue
415+
}
416+
417+
// The LinkFromOtherCache call above should've verified that
418+
// the package matches one of the hashes previously recorded,
419+
// if any. We'll now augment those hashes with one freshly
420+
// calculated from the package we just linked, which allows
421+
// the lock file to gradually transition to recording newer hash
422+
// schemes when they become available.
423+
var priorHashes []getproviders.Hash
424+
if lock != nil && lock.Version() == version {
425+
// If the version we're installing is identical to the
426+
// one we previously locked then we'll keep all of the
427+
// hashes we saved previously and add to it. Otherwise
428+
// we'll be starting fresh, because each version has its
429+
// own set of packages and thus its own hashes.
430+
priorHashes = append(priorHashes, preferredHashes...)
431+
432+
// NOTE: The behavior here is unfortunate when a particular
433+
// provider version was already cached on the first time
434+
// the current configuration requested it, because that
435+
// means we don't currently get the opportunity to fetch
436+
// and verify the checksums for the new package from
437+
// upstream. That's currently unavoidable because upstream
438+
// checksums are in the "ziphash" format and so we can't
439+
// verify them against our cache directory's unpacked
440+
// packages: we'd need to go fetch the package from the
441+
// origin and compare against it, which would defeat the
442+
// purpose of the global cache.
443+
//
444+
// If we fetch from upstream on the first encounter with
445+
// a particular provider then we'll end up in the other
446+
// codepath below where we're able to also include the
447+
// checksums from the origin registry.
448+
}
449+
newHash, err := cached.Hash()
450+
if err != nil {
451+
err := fmt.Errorf("after linking %s from provider cache at %s, failed to compute a checksum for it: %s", provider, i.globalCacheDir.baseDir, err)
452+
errs[provider] = err
453+
if cb := evts.LinkFromCacheFailure; cb != nil {
454+
cb(provider, version, err)
455+
}
456+
continue
457+
}
458+
// The hashes slice gets deduplicated in the lock file
459+
// implementation, so we don't worry about potentially
460+
// creating a duplicate here.
461+
var newHashes []getproviders.Hash
462+
newHashes = append(newHashes, priorHashes...)
463+
newHashes = append(newHashes, newHash)
464+
locks.SetProvider(provider, version, reqs[provider], newHashes)
465+
if cb := evts.ProvidersLockUpdated; cb != nil {
466+
// We want to ensure that newHash and priorHashes are
467+
// sorted. newHash is a single value, so it's definitely
468+
// sorted. priorHashes are pulled from the lock file, so
469+
// are also already sorted.
470+
cb(provider, version, []getproviders.Hash{newHash}, nil, priorHashes)
420471
}
421-
continue
422-
}
423-
// The hashes slice gets deduplicated in the lock file
424-
// implementation, so we don't worry about potentially
425-
// creating a duplicate here.
426-
var newHashes []getproviders.Hash
427-
newHashes = append(newHashes, priorHashes...)
428-
newHashes = append(newHashes, newHash)
429-
locks.SetProvider(provider, version, reqs[provider], newHashes)
430-
if cb := evts.ProvidersLockUpdated; cb != nil {
431-
// We want to ensure that newHash and priorHashes are
432-
// sorted. newHash is a single value, so it's definitely
433-
// sorted. priorHashes are pulled from the lock file, so
434-
// are also already sorted.
435-
cb(provider, version, []getproviders.Hash{newHash}, nil, priorHashes)
436-
}
437472

438-
if cb := evts.LinkFromCacheSuccess; cb != nil {
439-
cb(provider, version, new.PackageDir)
473+
if cb := evts.LinkFromCacheSuccess; cb != nil {
474+
cb(provider, version, new.PackageDir)
475+
}
476+
continue // Don't need to do full install, then.
440477
}
441-
continue // Don't need to do full install, then.
442478
}
443479
}
444480

@@ -491,7 +527,7 @@ NeedProvider:
491527
}
492528
new := installTo.ProviderVersion(provider, version)
493529
if new == nil {
494-
err := fmt.Errorf("after installing %s it is still not detected in the target directory; this is a bug in Terraform", provider)
530+
err := fmt.Errorf("after installing %s it is still not detected in %s; this is a bug in Terraform", provider, installTo.BasePath())
495531
errs[provider] = err
496532
if cb := evts.FetchPackageFailure; cb != nil {
497533
cb(provider, version, err)
@@ -521,6 +557,28 @@ NeedProvider:
521557
}
522558
continue
523559
}
560+
561+
// We should now also find the package in the linkTo dir, which
562+
// gives us the final value of "new" where the path points in to
563+
// the true target directory, rather than possibly the global
564+
// cache directory.
565+
new = linkTo.ProviderVersion(provider, version)
566+
if new == nil {
567+
err := fmt.Errorf("after installing %s it is still not detected in %s; this is a bug in Terraform", provider, linkTo.BasePath())
568+
errs[provider] = err
569+
if cb := evts.FetchPackageFailure; cb != nil {
570+
cb(provider, version, err)
571+
}
572+
continue
573+
}
574+
if _, err := new.ExecutableFile(); err != nil {
575+
err := fmt.Errorf("provider binary not found: %s", err)
576+
errs[provider] = err
577+
if cb := evts.FetchPackageFailure; cb != nil {
578+
cb(provider, version, err)
579+
}
580+
continue
581+
}
524582
}
525583
authResults[provider] = authResult
526584

0 commit comments

Comments
 (0)