Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

terraform init: add warning and guidance when lock file is incomplete #31399

Merged
merged 5 commits into from Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
85 changes: 73 additions & 12 deletions internal/command/init.go
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"fmt"
"log"
"reflect"
"sort"
"strings"

"github.com/hashicorp/hcl/v2"
Expand Down Expand Up @@ -548,6 +550,11 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State,
ctx, done := c.InterruptibleContext()
defer done()

// We want to print out a nice warning if we don't manage to pull
// checksums for all our providers. This is tracked via callbacks
// and incomplete providers are stored here for later analysis.
var incompleteProviders []string

// Because we're currently just streaming a series of events sequentially
// into the terminal, we're showing only a subset of the events to keep
// things relatively concise. Later it'd be nice to have a progress UI
Expand Down Expand Up @@ -789,6 +796,41 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State,

c.Ui.Info(fmt.Sprintf("- Installed %s v%s (%s%s)", provider.ForDisplay(), version, authResult, keyID))
},
ProvidersLockUpdated: func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash) {
// We're going to use this opportunity to track if we have any
// "incomplete" installs of providers. An incomplete install is
// when we are only going to write the local hashes into our lock
// file which means a `terraform init` command will fail in future
// when used on machines of a different architecture.
//
// We want to print a warning about this.

if len(signedHashes) > 0 {
// If we have any signedHashes hashes then we don't worry - as
// we know we retrieved all available hashes for this version
// anyway.
return
}

// If local hashes and prior hashes are exactly the same then
// it means we didn't record any signed hashes previously, and
// we know we're not adding any extra in now (because we already
// checked the signedHashes), so that's a problem.
//
// In the actual check here, if we have any priorHashes and those
// hashes are not the same as the local hashes then we're going to
// accept that this provider has been configured correctly.
if len(priorHashes) > 0 && !reflect.DeepEqual(localHashes, priorHashes) {
return
}

// Now, either signedHashes is empty, or priorHashes is exactly the
// same as our localHashes which means we never retrieved the
// signedHashes previously.
//
// Either way, this is bad. Let's complain/warn.
incompleteProviders = append(incompleteProviders, provider.ForDisplay())
},
ProvidersFetched: func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult) {
thirdPartySigned := false
for _, authResult := range authResults {
Expand All @@ -803,18 +845,6 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State,
"https://www.terraform.io/docs/cli/plugins/signing.html"))
}
},
HashPackageFailure: func(provider addrs.Provider, version getproviders.Version, err error) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Failed to validate installed provider",
fmt.Sprintf(
"Validating provider %s v%s failed: %s",
provider.ForDisplay(),
version,
err,
),
))
},
}
ctx = evts.OnContext(ctx)

Expand Down Expand Up @@ -874,6 +904,22 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State,
return true, false, diags
}

// Jump in here and add a warning if any of the providers are incomplete.
if len(incompleteProviders) > 0 {
// We don't really care about the order here, we just want the
// output to be deterministic.
sort.Slice(incompleteProviders, func(i, j int) bool {
return incompleteProviders[i] < incompleteProviders[j]
})
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
incompleteLockFileInformationHeader,
fmt.Sprintf(
incompleteLockFileInformationBody,
strings.Join(incompleteProviders, "\n - "),
getproviders.CurrentPlatform.String())))
}

if previousLocks.Empty() {
// A change from empty to non-empty is special because it suggests
// we're running "terraform init" for the first time against a
Expand Down Expand Up @@ -1195,3 +1241,18 @@ Alternatively, upgrade to the latest version of Terraform for compatibility with

// No version of the provider is compatible.
const errProviderVersionIncompatible = `No compatible versions of provider %s were found.`

// incompleteLockFileInformationHeader is the summary displayed to users when
// the lock file has only recorded local hashes.
const incompleteLockFileInformationHeader = `Incomplete lock file information for providers`

// incompleteLockFileInformationBody is the body of text displayed to users when
// the lock file has only recorded local hashes.
const incompleteLockFileInformationBody = `Due to your customized provider installation methods, Terraform was forced to calculate lock file checksums locally for the following providers:
- %s

The current .terraform.lock.hcl file only includes checksums for %s, so Terraform running running on another platform will fail to install these providers.
liamcervante marked this conversation as resolved.
Show resolved Hide resolved

To calculate additional checksums for another platform, run:
terraform providers lock -platform=linux_amd64
(where linux_amd64 is the platform to generate)`
11 changes: 7 additions & 4 deletions internal/command/init_test.go
Expand Up @@ -1644,7 +1644,7 @@ func TestInit_providerSource(t *testing.T) {
})
defer close()

ui := new(cli.MockUi)
ui := cli.NewMockUi()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you've discovered another example of the historical mistake of not properly initializing the mock UI! This does still come up from time to time, and indeed this is the way we typically fix it. (By which I mean: this probably won't be the last time you make a change like this!)

view, _ := testView(t)
m := Meta{
testingOverrides: metaOverridesForProvider(testProvider()),
Expand Down Expand Up @@ -1726,13 +1726,16 @@ func TestInit_providerSource(t *testing.T) {
},
),
}

if diff := cmp.Diff(gotProviderLocks, wantProviderLocks, depsfile.ProviderLockComparer); diff != "" {
t.Errorf("wrong version selections after upgrade\n%s", diff)
}

outputStr := ui.OutputWriter.String()
if want := "Installed hashicorp/test v1.2.3 (verified checksum)"; !strings.Contains(outputStr, want) {
t.Fatalf("unexpected output: %s\nexpected to include %q", outputStr, want)
if got, want := ui.OutputWriter.String(), "Installed hashicorp/test v1.2.3 (verified checksum)"; !strings.Contains(got, want) {
t.Fatalf("unexpected output: %s\nexpected to include %q", got, want)
}
if got, want := ui.ErrorWriter.String(), "\n - hashicorp/source\n - hashicorp/test\n - hashicorp/test-beta"; !strings.Contains(got, want) {
t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got)
}
}

Expand Down
38 changes: 32 additions & 6 deletions internal/providercache/installer.go
Expand Up @@ -385,14 +385,14 @@ NeedProvider:
// calculated from the package we just linked, which allows
// the lock file to gradually transition to recording newer hash
// schemes when they become available.
var newHashes []getproviders.Hash
var priorHashes []getproviders.Hash
if lock != nil && lock.Version() == version {
// If the version we're installing is identical to the
// one we previously locked then we'll keep all of the
// hashes we saved previously and add to it. Otherwise
// we'll be starting fresh, because each version has its
// own set of packages and thus its own hashes.
newHashes = append(newHashes, preferredHashes...)
priorHashes = append(priorHashes, preferredHashes...)

// NOTE: The behavior here is unfortunate when a particular
// provider version was already cached on the first time
Expand Down Expand Up @@ -423,8 +423,17 @@ NeedProvider:
// The hashes slice gets deduplicated in the lock file
// implementation, so we don't worry about potentially
// creating a duplicate here.
var newHashes []getproviders.Hash
newHashes = append(newHashes, priorHashes...)
newHashes = append(newHashes, newHash)
locks.SetProvider(provider, version, reqs[provider], newHashes)
if cb := evts.ProvidersLockUpdated; cb != nil {
// We want to ensure that newHash and priorHashes are
// sorted. newHash is a single value, so it's definitely
// sorted. priorHashes are pulled from the lock file, so
// are also already sorted.
cb(provider, version, []getproviders.Hash{newHash}, nil, priorHashes)
}

if cb := evts.LinkFromCacheSuccess; cb != nil {
cb(provider, version, new.PackageDir)
Expand Down Expand Up @@ -524,14 +533,14 @@ NeedProvider:
// The hashes slice gets deduplicated in the lock file
// implementation, so we don't worry about potentially
// creating duplicates here.
var newHashes []getproviders.Hash
var priorHashes []getproviders.Hash
if lock != nil && lock.Version() == version {
// If the version we're installing is identical to the
// one we previously locked then we'll keep all of the
// hashes we saved previously and add to it. Otherwise
// we'll be starting fresh, because each version has its
// own set of packages and thus its own hashes.
newHashes = append(newHashes, preferredHashes...)
priorHashes = append(priorHashes, preferredHashes...)
}
newHash, err := new.Hash()
if err != nil {
Expand All @@ -542,15 +551,32 @@ NeedProvider:
}
continue
}
newHashes = append(newHashes, newHash)

var signedHashes []getproviders.Hash
if authResult.SignedByAnyParty() {
// We'll trust new hashes from upstream only if they were verified
// as signed by a suitable key. Otherwise, we'd record only
// a new hash we just calculated ourselves from the bytes on disk,
// and so the hashes would cover only the current platform.
newHashes = append(newHashes, meta.AcceptableHashes()...)
signedHashes = append(signedHashes, meta.AcceptableHashes()...)
}

var newHashes []getproviders.Hash
newHashes = append(newHashes, newHash)
newHashes = append(newHashes, priorHashes...)
newHashes = append(newHashes, signedHashes...)

locks.SetProvider(provider, version, reqs[provider], newHashes)
if cb := evts.ProvidersLockUpdated; cb != nil {
// newHash and priorHashes are already sorted.
// But we do need to sort signedHashes so we can reason about it
// sensibly.
sort.Slice(signedHashes, func(i, j int) bool {
return string(signedHashes[i]) < string(signedHashes[j])
})

cb(provider, version, []getproviders.Hash{newHash}, signedHashes, priorHashes)
}

if cb := evts.FetchPackageSuccess; cb != nil {
cb(provider, version, new.PackageDir, authResult)
Expand Down
25 changes: 19 additions & 6 deletions internal/providercache/installer_events.go
Expand Up @@ -106,15 +106,28 @@ type InstallerEvents struct {
FetchPackageSuccess func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult)
FetchPackageFailure func(provider addrs.Provider, version getproviders.Version, err error)

// The ProvidersLockUpdated event is called whenever the lock file will be
// updated. It provides the following information:
//
// - `localHashes`: Hashes computed on the local system by analyzing
// files on disk.
// - `signedHashes`: Hashes signed by the private key that the origin
// registry claims is the owner of this provider.
// - `priorHashes`: Hashes already present in the lock file before we
// made any changes.
//
// The final lock file will be updated with a union of all the provided
// hashes. It not just likely, but expected that there will be duplicates
// shared between all three collections of hashes i.e. the local hash and
// remote hashes could already be in the cached hashes.
//
// In addition, we place a guarantee that the hash slices will be ordered
// in the same manner enforced by the lock file within NewProviderLock.
ProvidersLockUpdated func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash)

// The ProvidersFetched event is called after all fetch operations if at
// least one provider was fetched successfully.
ProvidersFetched func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult)

// HashPackageFailure is called if the installer is unable to determine
// the hash of the contents of an installed package after installation.
// In that case, the selection will not be recorded in the target cache
// directory's lock file.
HashPackageFailure func(provider addrs.Provider, version getproviders.Version, err error)
}

// OnContext produces a context with all of the same behaviors as the given
Expand Down
22 changes: 12 additions & 10 deletions internal/providercache/installer_events_test.go
Expand Up @@ -164,20 +164,22 @@ func installerLogEventsForTests(into chan<- *testInstallerEventLogItem) *Install
}{version.String(), err.Error()},
}
},
ProvidersFetched: func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult) {
ProvidersLockUpdated: func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash) {
into <- &testInstallerEventLogItem{
Event: "ProvidersFetched",
Args: authResults,
}
},
HashPackageFailure: func(provider addrs.Provider, version getproviders.Version, err error) {
into <- &testInstallerEventLogItem{
Event: "HashPackageFailure",
Event: "ProvidersLockUpdated",
Provider: provider,
Args: struct {
Version string
Error string
}{version.String(), err.Error()},
Local []getproviders.Hash
Signed []getproviders.Hash
Prior []getproviders.Hash
}{version.String(), localHashes, signedHashes, priorHashes},
}
},
ProvidersFetched: func(authResults map[addrs.Provider]*getproviders.PackageAuthenticationResult) {
into <- &testInstallerEventLogItem{
Event: "ProvidersFetched",
Args: authResults,
}
},
}
Expand Down