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

Ignore existing package hashes for providers lock command #31389

Merged
merged 4 commits into from Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from 2 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
51 changes: 46 additions & 5 deletions internal/command/providers_lock.go
Expand Up @@ -225,15 +225,15 @@ func (c *ProvidersLockCommand) Run(args []string) int {
if keyID != "" {
keyID = c.Colorize().Color(fmt.Sprintf(", key ID [reset][bold]%s[reset]", keyID))
}
c.Ui.Output(fmt.Sprintf("- Obtained %s checksums for %s (%s%s)", provider.ForDisplay(), platform, auth, keyID))
c.Ui.Output(fmt.Sprintf("- Retrieved %s %s for %s (%s%s)", provider.ForDisplay(), version, platform, auth, keyID))
},
}
ctx := evts.OnContext(ctx)

dir := providercache.NewDirWithPlatform(tempDir, platform)
installer := providercache.NewInstaller(dir, source)

newLocks, err := installer.EnsureProviderVersions(ctx, oldLocks, reqs, providercache.InstallNewProvidersOnly)
newLocks, err := installer.EnsureProviderVersions(ctx, oldLocks, reqs, providercache.InstallNewProvidersForce)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
Expand All @@ -252,6 +252,10 @@ func (c *ProvidersLockCommand) Run(args []string) int {
return 1
}

// Track whether we've made any changes to the lock file as part of this
// operation. We can customise the final message based on our actions.
madeAnyChange := false

// We now have a separate updated locks object for each platform. We need
// to merge those all together so that the final result has the union of
// all of the checksums we saw for each of the providers we've worked on.
Expand All @@ -270,7 +274,7 @@ func (c *ProvidersLockCommand) Run(args []string) int {
constraints = oldLock.VersionConstraints()
hashes = append(hashes, oldLock.AllHashes()...)
}
for _, platformLocks := range updatedLocks {
for platform, platformLocks := range updatedLocks {
platformLock := platformLocks.Provider(provider)
if platformLock == nil {
continue // weird, but we'll tolerate it to avoid crashing
Expand All @@ -282,6 +286,39 @@ func (c *ProvidersLockCommand) Run(args []string) int {
// platforms here, because the SetProvider method we call below
// handles that automatically.
hashes = append(hashes, platformLock.AllHashes()...)

// At this point, we've merged all the hashes for this (provider, platform)
// combo into the combined hashes for this provider. Let's take this
// opportunity to print out a summary for this particular combination.

if oldLock == nil {
// Then we weren't tracking this provider previously so we can write a simple error message.
madeAnyChange = true
c.Ui.Output(
fmt.Sprintf(
"- Obtained %s checksums for %s; This was a new provider and the checksums for this platform are now tracked in the lock file",
provider.ForDisplay(),
platform))
continue
}

if oldLock.ContainsAll(platformLock) {
Copy link
Member

Choose a reason for hiding this comment

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

My understanding of this call and the function it's calling is that it will return true only if platformLock doesn't add any new hashes compared to oldLock, which feels opposite to the message it's printing. Am I misunderstanding the behavior of ContainsAll? 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

Good spot! I had my logic the wrong way, I've fixed it now. I followed the second idea, so split this out into a function and added some tests. Left a simple switch statement in the original function.

// Then we found new checksums for this provider and platform that weren't tracked previously.
madeAnyChange = true
c.Ui.Output(
fmt.Sprintf(
"- Obtained %s checksums for %s; Additional checksums for this platform are now tracked in the lock file",
provider.ForDisplay(),
platform))
continue
}

// Finally, if we reach here it means we found no new checksums so let's explain that.
c.Ui.Output(
fmt.Sprintf(
"- Obtained %s checksums for %s; All checksums for this platform were already tracked in the lock file",
provider.ForDisplay(),
platform))
}
newLocks.SetProvider(provider, version, constraints, hashes)
}
Expand All @@ -294,8 +331,12 @@ func (c *ProvidersLockCommand) Run(args []string) int {
return 1
}

c.Ui.Output(c.Colorize().Color("\n[bold][green]Success![reset] [bold]Terraform has updated the lock file.[reset]"))
c.Ui.Output("\nReview the changes in .terraform.lock.hcl and then commit to your\nversion control system to retain the new checksums.\n")
if madeAnyChange {
c.Ui.Output(c.Colorize().Color("\n[bold][green]Success![reset] [bold]Terraform has updated the lock file.[reset]"))
c.Ui.Output("\nReview the changes in .terraform.lock.hcl and then commit to your\nversion control system to retain the new checksums.\n")
} else {
c.Ui.Output(c.Colorize().Color("\n[bold][green]Success![reset] [bold]Terraform has validated the lock file and found no need for changes.[reset]"))
}
return 0
}

Expand Down
92 changes: 57 additions & 35 deletions internal/command/providers_lock_test.go
Expand Up @@ -33,56 +33,78 @@ func TestProvidersLock(t *testing.T) {

// This test depends on the -fs-mirror argument, so we always know what results to expect
t.Run("basic", func(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("providers-lock/basic"), td)
defer testChdir(t, td)()

// Our fixture dir has a generic os_arch dir, which we need to customize
// to the actual OS/arch where this test is running in order to get the
// desired result.
fixtMachineDir := filepath.Join(td, "fs-mirror/registry.terraform.io/hashicorp/test/1.0.0/os_arch")
wantMachineDir := filepath.Join(td, "fs-mirror/registry.terraform.io/hashicorp/test/1.0.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
err := os.Rename(fixtMachineDir, wantMachineDir)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}

p := testProvider()
ui := new(cli.MockUi)
c := &ProvidersLockCommand{
Meta: Meta{
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
},
}

args := []string{"-fs-mirror=fs-mirror"}
code := c.Run(args)
if code != 0 {
t.Fatalf("wrong exit code; expected 0, got %d", code)
}
testDirectory := "providers-lock/basic"
expected := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

lockfile, err := os.ReadFile(".terraform.lock.hcl")
if err != nil {
t.Fatal("error reading lockfile")
}
provider "registry.terraform.io/hashicorp/test" {
version = "1.0.0"
hashes = [
"h1:7MjN4eFisdTv4tlhXH5hL4QQd39Jy4baPhFxwAd/EFE=",
]
}
`
runProviderLockGenericTest(t, testDirectory, expected)
})

// This test depends on the -fs-mirror argument, so we always know what results to expect
t.Run("append", func(t *testing.T) {
testDirectory := "providers-lock/append"
expected := `# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.

provider "registry.terraform.io/hashicorp/test" {
version = "1.0.0"
hashes = [
"h1:7MjN4eFisdTv4tlhXH5hL4QQd39Jy4baPhFxwAd/EFE=",
"h1:invalid",
]
}
`
if string(lockfile) != expected {
t.Fatalf("wrong lockfile content")
}
runProviderLockGenericTest(t, testDirectory, expected)
})
}

func runProviderLockGenericTest(t *testing.T, testDirectory, expected string) {
td := t.TempDir()
testCopyDir(t, testFixturePath(testDirectory), td)
defer testChdir(t, td)()

// Our fixture dir has a generic os_arch dir, which we need to customize
// to the actual OS/arch where this test is running in order to get the
// desired result.
fixtMachineDir := filepath.Join(td, "fs-mirror/registry.terraform.io/hashicorp/test/1.0.0/os_arch")
wantMachineDir := filepath.Join(td, "fs-mirror/registry.terraform.io/hashicorp/test/1.0.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
err := os.Rename(fixtMachineDir, wantMachineDir)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}

p := testProvider()
ui := new(cli.MockUi)
c := &ProvidersLockCommand{
Meta: Meta{
Ui: ui,
testingOverrides: metaOverridesForProvider(p),
},
}

args := []string{"-fs-mirror=fs-mirror"}
code := c.Run(args)
if code != 0 {
t.Fatalf("wrong exit code; expected 0, got %d", code)
}

lockfile, err := os.ReadFile(".terraform.lock.hcl")
if err != nil {
t.Fatal("error reading lockfile")
}

if string(lockfile) != expected {
t.Fatalf("wrong lockfile content")
}
}

func TestProvidersLock_args(t *testing.T) {

t.Run("mirror collision", func(t *testing.T) {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions internal/command/testdata/providers-lock/append/main.tf
@@ -0,0 +1,7 @@
terraform {
required_providers {
test = {
source = "hashicorp/test"
}
}
}
24 changes: 24 additions & 0 deletions internal/depsfile/locks.go
Expand Up @@ -403,6 +403,30 @@ 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 guarenteed 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
Expand Down
91 changes: 91 additions & 0 deletions internal/depsfile/locks_test.go
Expand Up @@ -216,3 +216,94 @@ func TestLocksProviderSetRemove(t *testing.T) {
}
}
}

func TestProviderLockContainsAll(t *testing.T) {
provider := addrs.NewDefaultProvider("provider")
v2 := getproviders.MustParseVersion("2.0.0")
v2EqConstraints := getproviders.MustParseVersionConstraints("2.0.0")

t.Run("non-symmetric", func(t *testing.T) {
target := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
"9r3i9a9QmASqMnQM",
"K43RHM2klOoywtyW",
"swJPXfuCNhJsTM5c",
})

original := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
"9r3i9a9QmASqMnQM",
"1ZAChGWUMWn4zmIk",
"K43RHM2klOoywtyW",
"HWjRvIuWZ1LVatnc",
"swJPXfuCNhJsTM5c",
"KwhJK4p/U2dqbKhI",
})

if !original.ContainsAll(target) {
t.Fatalf("orginal should contain all hashes in target")
}
if target.ContainsAll(original) {
t.Fatalf("target should not contain all hashes in orginal")
}
Copy link
Member

Choose a reason for hiding this comment

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

Minor: this seems like a good opportunity to use t.Errorf to allow both of these to potentially fail together if the function really misbehaves, since these two checks seem independent.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

})

t.Run("symmetric", func(t *testing.T) {
target := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
"9r3i9a9QmASqMnQM",
"K43RHM2klOoywtyW",
"swJPXfuCNhJsTM5c",
})

original := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
"9r3i9a9QmASqMnQM",
"K43RHM2klOoywtyW",
"swJPXfuCNhJsTM5c",
})

if !original.ContainsAll(target) {
t.Fatalf("orginal should contain all hashes in target")
}
if !target.ContainsAll(original) {
t.Fatalf("target should not contain all hashes in orginal")
}
})

t.Run("edge case - null", func(t *testing.T) {
original := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
"9r3i9a9QmASqMnQM",
"K43RHM2klOoywtyW",
"swJPXfuCNhJsTM5c",
})

if !original.ContainsAll(nil) {
t.Fatalf("orginal should report true on nil")
}
})

t.Run("edge case - empty", func(t *testing.T) {
original := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
"9r3i9a9QmASqMnQM",
"K43RHM2klOoywtyW",
"swJPXfuCNhJsTM5c",
})

target := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{})

if !original.ContainsAll(target) {
t.Fatalf("orginal should report true on empty")
}
})

t.Run("edge case - original empty", func(t *testing.T) {
original := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{})

target := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
"9r3i9a9QmASqMnQM",
"K43RHM2klOoywtyW",
"swJPXfuCNhJsTM5c",
})

if original.ContainsAll(target) {
t.Fatalf("orginal should report false when empty")
}
})
}
17 changes: 16 additions & 1 deletion internal/providercache/installer.go
Expand Up @@ -464,7 +464,13 @@ NeedProvider:
installTo = i.targetDir
linkTo = nil // no linking needed
}
authResult, err := installTo.InstallPackage(ctx, meta, preferredHashes)

allowedHashes := preferredHashes
if mode.forceInstallChecksums() {
allowedHashes = []getproviders.Hash{}
}

authResult, err := installTo.InstallPackage(ctx, meta, allowedHashes)
if err != nil {
// TODO: Consider retrying for certain kinds of error that seem
// likely to be transient. For now, we just treat all errors equally.
Expand Down Expand Up @@ -594,6 +600,11 @@ const (
// sets.
InstallNewProvidersOnly InstallMode = 'N'

// InstallNewProvidersForce is an InstallMode that follows the same
// logic as InstallNewProvidersOnly except it does not verify existing
// checksums but force installs new checksums for all given providers.
InstallNewProvidersForce InstallMode = 'F'

// InstallUpgrades is an InstallMode that causes the installer to check
// all requested providers to see if new versions are available that
// are also in the given version sets, even if a suitable version of
Expand All @@ -605,6 +616,10 @@ func (m InstallMode) forceQueryAllProviders() bool {
return m == InstallUpgrades
}

func (m InstallMode) forceInstallChecksums() bool {
return m == InstallNewProvidersForce
}

// InstallerError is an error type that may be returned (but is not guaranteed)
// from Installer.EnsureProviderVersions to indicate potentially several
// separate failed installation outcomes for different providers included in
Expand Down