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 3 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
77 changes: 72 additions & 5 deletions internal/command/providers_lock.go
Expand Up @@ -13,6 +13,14 @@ import (
"github.com/hashicorp/terraform/internal/tfdiags"
)

type providersLockChangeType string

const (
providersLockChangeTypeNoChange providersLockChangeType = "providersLockChangeTypeNoChange"
providersLockChangeTypeNewProvider providersLockChangeType = "providersLockChangeTypeNewProvider"
providersLockChangeTypeNewHashes providersLockChangeType = "providersLockChangeTypeNewHashes"
)

// ProvidersLockCommand is a Command implementation that implements the
// "terraform providers lock" command, which creates or updates the current
// configuration's dependency lock file using information from upstream
Expand Down Expand Up @@ -225,15 +233,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 +260,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 +282,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 +294,32 @@ 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.
switch providersLockCalculateChangeType(oldLock, platformLock) {
case providersLockChangeTypeNewProvider:
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))
case providersLockChangeTypeNewHashes:
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))
case providersLockChangeTypeNoChange:
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 +332,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 Expand Up @@ -357,3 +399,28 @@ Options:
set of target platforms.
`
}

// providersLockCalculateChangeType works out whether there is any difference
// between oldLock and newLock and returns a variable the main function can use
// to decide on which message to print.
//
// One assumption made here that is not obvious without the context from the
// main function is that while platformLock contains the lock information for a
// single platform after the current run, oldLock contains the combined
// information of all platforms from when the versions were last checked. A
// simple equality check is not sufficient for deciding on change as we expect
// that oldLock will be a superset of platformLock if no new hashes have been
// found.
//
// We've separated this function out so we can write unit tests around the
// logic. This function assumes the platformLock is not nil, as the main
// function explicitly checks this before calling this function.
func providersLockCalculateChangeType(oldLock *depsfile.ProviderLock, platformLock *depsfile.ProviderLock) providersLockChangeType {
if oldLock == nil {
return providersLockChangeTypeNewProvider
}
if oldLock.ContainsAll(platformLock) {
return providersLockChangeTypeNoChange
}
return providersLockChangeTypeNewHashes
}
173 changes: 138 additions & 35 deletions internal/command/providers_lock_test.go
Expand Up @@ -2,6 +2,9 @@ package command

import (
"fmt"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders"
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't look like what our usual tools would generate, but I must admit I'm not entirely sure what usual tool is responsible for it, and since the checks all passed I guess it might be something we have in our local editors only rather than something enforced by our CI process. 😖

Our typical basic style here is to separate the standard library packages from the external ones, with the standard library ones first and then a blank line followed by the external ones, with each separate "block" of packages in lexical order. I'd suggest manually changing this to match that for now, and then separately perhaps we can figure out which of the tools we're missing from our CI checks to catch this automatically in the future.

For some files that have a lot of imports we do also sometimes split the Terraform-internal packages from other external packages, so there would then be three "blocks" of packages: standard library, external, and then Terraform-internal. However, that's a more subjective call that our tooling does not enforce, and this one doesn't seem like it has enough packages to warrant that amount of fussiness, so I'm mentioning it only in the interests of helping you to see the patterns in other files in future. 😀

Copy link
Member

Choose a reason for hiding this comment

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

The "default" grouping for imports is whatever goimports creates automatically, which is usually bundled with the standard group of editor plugins. The CI checks only verify what go fmt produces, which does not re-order imports on its own. It would be good to get that tool (or equivalent) working, since anyone else opening the file is going to rewrite it in their next commit.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed! And yeah it looks like goimports isn't installed by Goland automatically: https://www.jetbrains.com/help/go/integration-with-go-tools.html#goimports

I have now configured it.

Brief sidebar into/shoutout for https://pre-commit.com/ as something I've used in the past to make sure CI/CD and pre-commit checks on engineers machines are consistent (works across IDEs/architectures, forces common versions for all tools, etc).

"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -33,56 +36,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 Expand Up @@ -151,3 +176,81 @@ func TestProvidersLock_args(t *testing.T) {
}
})
}

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

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

if ct := providersLockCalculateChangeType(nil, platformLock); ct != providersLockChangeTypeNewProvider {
t.Fatalf("output was %s but should be %s", ct, providersLockChangeTypeNewProvider)
}
})

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

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

if ct := providersLockCalculateChangeType(oldLock, platformLock); ct != providersLockChangeTypeNoChange {
t.Fatalf("output was %s but should be %s", ct, providersLockChangeTypeNoChange)
}
})

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

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

if ct := providersLockCalculateChangeType(oldLock, platformLock); ct != providersLockChangeTypeNoChange {
t.Fatalf("output was %s but should be %s", ct, providersLockChangeTypeNoChange)
}
})

t.Run("oldLock < platformLock", func(t *testing.T) {
platformLock := depsfile.NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{
"9r3i9a9QmASqMnQM",
"1ZAChGWUMWn4zmIk",
"K43RHM2klOoywtyW",
"HWjRvIuWZ1LVatnc",
"swJPXfuCNhJsTM5c",
"KwhJK4p/U2dqbKhI",
})

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

if ct := providersLockCalculateChangeType(oldLock, platformLock); ct != providersLockChangeTypeNewHashes {
t.Fatalf("output was %s but should be %s", ct, providersLockChangeTypeNoChange)
}
})
}

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 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
Expand Down