test: Add tests defining how provider installation behaviour is affected by providers that are either reattached or dev_overrides (#38633)

* test: Add test coverage to provider installer logic defining how providers being unmanaged/reattached or dev_overrides impacts the provider installation process

These tests show that the two methods of overriding a provider behave differently. This is due to how reattached/unmanaged providers are skipped during the provider installation process whereas development overrides are removed from the provider requirements passed into the installation logic. This results in Terraform treating dev_override providers similarly to a provider that's been completely removed from a configuration. I believe this is a bug and will fix in a future commit, but these tests help to highlight the current problem.
This commit is contained in:
Sarah French 2026-05-26 15:41:27 +01:00 committed by GitHub
parent c4dda736bf
commit 8410263caa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 803 additions and 0 deletions

View file

@ -4,13 +4,29 @@
package e2etest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/e2e"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
"github.com/hashicorp/terraform/internal/grpcwrap"
tfplugin "github.com/hashicorp/terraform/internal/plugin6"
simple "github.com/hashicorp/terraform/internal/provider-simple-v6"
proto "github.com/hashicorp/terraform/internal/tfplugin6"
)
// TestProviderProtocols verifies that Terraform can execute provider plugins
@ -88,3 +104,582 @@ func TestProviderProtocols(t *testing.T) {
t.Fatalf("wrong destroy output\nstdout:%s\nstderr:%s", stdout, stderr)
}
}
// TestProviderInstall_dev_override verifies provider plugin installation behaviour
// when a dev_override is in use.
func TestProviderInstall_dev_override(t *testing.T) {
if !canRunGoBuild {
// We're running in a separate-build-then-run context, so we can't
// currently execute this test which depends on being able to build
// new executable at runtime.
//
// (See the comment on canRunGoBuild's declaration for more information.)
t.Skip("can't run without building a new provider executable")
}
fixturePath := "testdata/provider-plugin" // Reused
// In temp dir create a plugin cache to be used in the test cases.
// The cache is supplied to commands using the -plugin-dir init flag.
// There are 4 providers total:
// - simple provider with versions 1.0.0 and 2.0.0 available
// - simple6 provider with versions 1.0.0 and 2.0.0 available
td := t.TempDir()
providerVersionOld := "1.0.0"
providerVersionNew := "2.0.0"
platform := getproviders.CurrentPlatform.String()
absolutePathToCache := filepath.Join(td, "cache")
simple5Provider := filepath.Join(td, "terraform-provider-simple")
simple5ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple/main", simple5Provider)
for _, v := range []string{providerVersionOld, providerVersionNew} {
dir := filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple", v, platform)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
t.Fatal(err)
}
// Create an executable copy of the simple5ProviderExe file per version in the cache dir
data, err := os.ReadFile(simple5ProviderExe)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "terraform-provider-simple"), data, 0755); err != nil {
t.Fatal(err)
}
}
simple6Provider := filepath.Join(td, "terraform-provider-simple6")
simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider)
for _, v := range []string{providerVersionOld, providerVersionNew} {
dir := filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple6", v, platform)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
t.Fatal(err)
}
// Create an executable copy of the simple6ProviderExe file per version in the cache dir
data, err := os.ReadFile(simple6ProviderExe)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "terraform-provider-simple6"), data, 0755); err != nil {
t.Fatal(err)
}
}
// Get hashes of 3 of the 4 providers
// These are used when creating or asserting against lock files.
var simple5v1_0_0Hash providerreqs.Hash
var simple6v1_0_0Hash providerreqs.Hash
var simple5v2_0_0Hash providerreqs.Hash
var err error
loc := getproviders.PackageLocalDir(filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple", providerVersionOld, platform))
if simple5v1_0_0Hash, err = getproviders.PackageHash(loc); err != nil {
t.Fatal(err)
}
loc = getproviders.PackageLocalDir(filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple", providerVersionNew, platform))
if simple5v2_0_0Hash, err = getproviders.PackageHash(loc); err != nil {
t.Fatal(err)
}
loc = getproviders.PackageLocalDir(filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple6", providerVersionOld, platform))
if simple6v1_0_0Hash, err = getproviders.PackageHash(loc); err != nil {
t.Fatal(err)
}
// Set up a reused CLI configuration file that sets simple6 as a dev_override,
// Tests will use this via the TF_CLI_CONFIG_FILE environment variable.
cliCfg := fmt.Sprintf(`provider_installation {
dev_overrides {
"hashicorp/simple6" = "%s"
}
# For all other providers, install them directly from their origin provider
# registries as normal. If you omit this, Terraform will _only_ use
# the dev_overrides block, and so no other providers will be available.
direct {}
}
`, filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple6", providerVersionOld, platform))
cliConfigFilePath := filepath.Join(td, "dev_override.tfrc")
if err := os.WriteFile(cliConfigFilePath, []byte(cliCfg), 0644); err != nil {
t.Fatalf("err: %s", err)
}
t.Run("dev_override not installed during init when provider not present in dependency lock file", func(t *testing.T) {
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
tf := e2e.NewBinary(t, terraformBin, fixturePath)
// There is no lock file present at start
lockFile := filepath.Join(tf.WorkDir(), ".terraform.lock.hcl")
_, err := os.Stat(lockFile)
if err == nil {
t.Fatal("expected error due to file not existing, got no error")
}
if !os.IsNotExist(err) {
t.Fatalf("expected error due to file not existing, got different error: %s", err)
}
// The simple6 provider is a dev_override
tf.AddEnv("TF_CLI_CONFIG_FILE=" + cliConfigFilePath)
// The init process should succeed.
stdout, stderr, err := tf.Run("init", fmt.Sprintf("-plugin-dir=%s", absolutePathToCache))
if err != nil {
t.Fatalf("unexpected error: %s\nstdout: %s\nstderr: %s", err, stdout, stderr)
}
// Lockfile is created
// simple provider is installed using the latest, 2.0.0 version,
// but the dev_override simple6 provider is not added to the lockfile.
buf, err := os.ReadFile(lockFile)
if err != nil {
t.Fatalf("unexpected error accessing lock file: %s", err)
}
buf = bytes.TrimSpace(buf)
expectedLockFileContent := fmt.Sprintf(`# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/simple" {
version = "2.0.0"
hashes = [
"%s",
]
}`,
simple5v2_0_0Hash,
)
if diff := cmp.Diff(expectedLockFileContent, string(buf)); diff != "" {
t.Errorf("unexpected difference in lock file content: %s", diff)
}
})
t.Run("dev_override causes provider to be removed from dependency lock file during init", func(t *testing.T) {
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
tf := e2e.NewBinary(t, terraformBin, fixturePath)
// Lockfile contains both simple and simple6 providers already
priorLockFile := fmt.Sprintf(`# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/simple" {
version = "1.0.0"
hashes = [
"%s",
]
}
provider "registry.terraform.io/hashicorp/simple6" {
version = "1.0.0"
hashes = [
"%s",
]
}`,
simple5v1_0_0Hash,
simple6v1_0_0Hash,
)
lockFile := filepath.Join(tf.WorkDir(), ".terraform.lock.hcl")
if err := os.WriteFile(lockFile, []byte(priorLockFile), 0644); err != nil {
t.Fatalf("error writing prior lock file: %s", err)
}
// The simple6 provider is a dev_override
tf.AddEnv("TF_CLI_CONFIG_FILE=" + cliConfigFilePath)
// The init process should succeed.
stdout, stderr, err := tf.Run("init", fmt.Sprintf("-plugin-dir=%s", absolutePathToCache))
if err != nil {
t.Fatalf("unexpected error: %s\nstdout: %s\nstderr: %s", err, stdout, stderr)
}
// Lockfile has been altered to remove the simple6 provider
buf, err := os.ReadFile(lockFile)
if err != nil {
t.Fatalf("unexpected error accessing lock file: %s", err)
}
buf = bytes.TrimSpace(buf)
expectedLockFile := fmt.Sprintf(`# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/simple" {
version = "1.0.0"
hashes = [
"%s",
]
}`,
simple5v1_0_0Hash,
)
if diff := cmp.Diff(expectedLockFile, string(buf)); diff != "" {
t.Fatalf("unexpected difference in lock file content: %s", diff)
}
})
t.Run("dev_override also causes provider to be removed from dependency lock file during init -upgrade", func(t *testing.T) {
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
tf := e2e.NewBinary(t, terraformBin, fixturePath)
// Lockfile contains both simple and simple6 providers already
priorLockFile := fmt.Sprintf(`# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/simple" {
version = "1.0.0"
hashes = [
"%s",
]
}
provider "registry.terraform.io/hashicorp/simple6" {
version = "1.0.0"
hashes = [
"%s",
]
}`,
simple5v1_0_0Hash,
simple6v1_0_0Hash,
)
lockFile := filepath.Join(tf.WorkDir(), ".terraform.lock.hcl")
if err := os.WriteFile(lockFile, []byte(priorLockFile), 0644); err != nil {
t.Fatalf("error writing prior lock file: %s", err)
}
// The simple6 provider is a dev_override
tf.AddEnv("TF_CLI_CONFIG_FILE=" + cliConfigFilePath)
// The init -upgrade process should succeed.
stdout, stderr, err := tf.Run("init", "-upgrade", fmt.Sprintf("-plugin-dir=%s", absolutePathToCache))
if err != nil {
t.Fatalf("unexpected error: %s\nstdout: %s\nstderr: %s", err, stdout, stderr)
}
// Lockfile shows evidence of upgrade process
// simple provider is upgraded to the newer 2.0.0 version,
// but the dev_override simple6 provider is removed from the lockfile.
buf, err := os.ReadFile(lockFile)
if err != nil {
t.Fatalf("unexpected error accessing lock file: %s", err)
}
buf = bytes.TrimSpace(buf)
expectedLockFileContent := fmt.Sprintf(`# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/simple" {
version = "2.0.0"
hashes = [
"%s",
]
}`,
simple5v2_0_0Hash,
)
if diff := cmp.Diff(expectedLockFileContent, string(buf)); diff != "" {
t.Errorf("unexpected difference in lock file content: %s", diff)
}
})
}
// TestProviderInstall_reattached verifies provider plugin installation behaviour
// when a reattached/unmanaged provider is in use.
func TestProviderInstall_reattached(t *testing.T) {
if !canRunGoBuild {
// We're running in a separate-build-then-run context, so we can't
// currently execute this test which depends on being able to build
// new executable at runtime.
//
// (See the comment on canRunGoBuild's declaration for more information.)
t.Skip("can't run without building a new provider executable")
}
fixturePath := "testdata/provider-plugin" // Reused
// In temp dir create a plugin cache to be used in the test cases.
// The cache is supplied to commands using the -plugin-dir init flag.
// There are 4 providers total:
// - simple provider with versions 1.0.0 and 2.0.0 available
// - simple6 provider with versions 1.0.0 and 2.0.0 available
td := t.TempDir()
providerVersionOld := "1.0.0"
providerVersionNew := "2.0.0"
platform := getproviders.CurrentPlatform.String()
absolutePathToCache := filepath.Join(td, "cache")
simple5Provider := filepath.Join(td, "terraform-provider-simple")
simple5ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple/main", simple5Provider)
for _, v := range []string{providerVersionOld, providerVersionNew} {
dir := filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple", v, platform)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
t.Fatal(err)
}
// Create an executable copy of the simple5ProviderExe file per version in the cache dir
data, err := os.ReadFile(simple5ProviderExe)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "terraform-provider-simple"), data, 0755); err != nil {
t.Fatal(err)
}
}
simple6Provider := filepath.Join(td, "terraform-provider-simple6")
simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider)
for _, v := range []string{providerVersionOld, providerVersionNew} {
dir := filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple6", v, platform)
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
t.Fatal(err)
}
// Create an executable copy of the simple6ProviderExe file per version in the cache dir
data, err := os.ReadFile(simple6ProviderExe)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "terraform-provider-simple6"), data, 0755); err != nil {
t.Fatal(err)
}
}
// Get hashes of 3 of the 4 providers
// These are used when creating or asserting against lock files.
var simple5v1_0_0Hash providerreqs.Hash
var simple6v1_0_0Hash providerreqs.Hash
var simple5v2_0_0Hash providerreqs.Hash
var err error
loc := getproviders.PackageLocalDir(filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple", providerVersionOld, platform))
if simple5v1_0_0Hash, err = getproviders.PackageHash(loc); err != nil {
t.Fatal(err)
}
loc = getproviders.PackageLocalDir(filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple", providerVersionNew, platform))
if simple5v2_0_0Hash, err = getproviders.PackageHash(loc); err != nil {
t.Fatal(err)
}
loc = getproviders.PackageLocalDir(filepath.Join(absolutePathToCache, "registry.terraform.io/hashicorp", "simple6", providerVersionOld, platform))
if simple6v1_0_0Hash, err = getproviders.PackageHash(loc); err != nil {
t.Fatal(err)
}
// Launch a separate simple6 provider process to be re-used as a reattached provider.
// Tests will use this via the TF_REATTACH_PROVIDERS environment variable.
reattachConfig := reattachedProviderForTest(t, addrs.NewDefaultProvider("simple6"), 6)
t.Run("reattached provider not installed when provider not present in dependency lock file", func(t *testing.T) {
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
tf := e2e.NewBinary(t, terraformBin, fixturePath)
// There is no lock file present at start
lockFile := filepath.Join(tf.WorkDir(), ".terraform.lock.hcl")
_, err := os.Stat(lockFile)
if err == nil {
t.Fatal("expected error due to file not existing, got no error")
}
if !os.IsNotExist(err) {
t.Fatalf("expected error due to file not existing, got different error: %s", err)
}
// The simple6 provider is reattached/unmanaged
tf.AddEnv("TF_REATTACH_PROVIDERS=" + reattachConfig)
// The init process should succeed.
stdout, stderr, err := tf.Run("init", fmt.Sprintf("-plugin-dir=%s", absolutePathToCache))
if err != nil {
t.Fatalf("unexpected error: %s\nstdout: %s\nstderr: %s", err, stdout, stderr)
}
// Lock file should have been created
buf, err := os.ReadFile(lockFile)
if err != nil {
t.Fatalf("unexpected error accessing lock file: %s", err)
}
buf = bytes.TrimSpace(buf)
// We expect the lock file to not contain the simple6 provider that's being reattached/unmanaged,
// because that provider is skipped during the installation process.
// The simple (v5) provider is installed as usual, pulling in the latest version.
expectedLockFileContent := fmt.Sprintf(`# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/simple" {
version = "2.0.0"
hashes = [
"%s",
]
}`, simple5v2_0_0Hash)
if diff := cmp.Diff(expectedLockFileContent, string(buf)); diff != "" {
t.Errorf("unexpected difference in lock file content: %s", diff)
}
})
t.Run("reattached providers do NOT cause provider to be removed from dependency lock file during init", func(t *testing.T) {
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
tf := e2e.NewBinary(t, terraformBin, fixturePath)
// Lockfile contains both simple and simple6 providers already
priorLockFile := fmt.Sprintf(`# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/simple" {
version = "1.0.0"
hashes = [
"%s",
]
}
provider "registry.terraform.io/hashicorp/simple6" {
version = "1.0.0"
hashes = [
"%s",
]
}`,
simple5v1_0_0Hash,
simple6v1_0_0Hash,
)
lockFile := filepath.Join(tf.WorkDir(), ".terraform.lock.hcl")
if err := os.WriteFile(lockFile, []byte(priorLockFile), 0644); err != nil {
t.Fatalf("error writing prior lock file: %s", err)
}
// The simple6 provider is reattached/unmanaged
tf.AddEnv("TF_REATTACH_PROVIDERS=" + reattachConfig)
// The init process should succeed.
stdout, stderr, err := tf.Run("init", fmt.Sprintf("-plugin-dir=%s", absolutePathToCache))
if err != nil {
t.Fatalf("unexpected error: %s\nstdout: %s\nstderr: %s", err, stdout, stderr)
}
// Lockfile is unchanged despite use of a reattached/unmanaged simple6 provider
buf, err := os.ReadFile(lockFile)
if err != nil {
t.Fatalf("unexpected error accessing lock file: %s", err)
}
buf = bytes.TrimSpace(buf)
if diff := cmp.Diff(priorLockFile, string(buf)); diff != "" {
t.Fatalf("unexpected difference in lock file content: %s", diff)
}
})
t.Run("reattached providers are unchanged in the dependency lock file during init -upgrade", func(t *testing.T) {
terraformBin := e2e.GoBuild("github.com/hashicorp/terraform", "terraform")
tf := e2e.NewBinary(t, terraformBin, fixturePath)
// Lockfile contains both simple and simple6 providers already at older version 1.0.0
priorLockFile := fmt.Sprintf(`# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/simple" {
version = "1.0.0"
hashes = [
"%s",
]
}
provider "registry.terraform.io/hashicorp/simple6" {
version = "1.0.0"
hashes = [
"%s",
]
}`,
simple5v1_0_0Hash,
simple6v1_0_0Hash,
)
lockFile := filepath.Join(tf.WorkDir(), ".terraform.lock.hcl")
if err := os.WriteFile(lockFile, []byte(priorLockFile), 0644); err != nil {
t.Fatalf("error writing prior lock file: %s", err)
}
// The simple6 provider is reattached/unmanaged
tf.AddEnv("TF_REATTACH_PROVIDERS=" + reattachConfig)
// The init -upgrade process should succeed.
stdout, stderr, err := tf.Run("init", "-upgrade", fmt.Sprintf("-plugin-dir=%s", absolutePathToCache))
if err != nil {
t.Fatalf("unexpected error: %s\nstdout: %s\nstderr: %s", err, stdout, stderr)
}
// Lockfile shows evidence of upgrade process
// simple provider is upgraded to the newer 2.0.0 version,
// but the reattached simple6 provider is unchanged due to being reattached.
buf, err := os.ReadFile(lockFile)
if err != nil {
t.Fatalf("unexpected error accessing lock file: %s", err)
}
buf = bytes.TrimSpace(buf)
expectedLockFileContent := fmt.Sprintf(`# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/simple" {
version = "2.0.0"
hashes = [
"%s",
]
}
provider "registry.terraform.io/hashicorp/simple6" {
version = "1.0.0"
hashes = [
"%s",
]
}`,
simple5v2_0_0Hash,
simple6v1_0_0Hash,
)
if diff := cmp.Diff(expectedLockFileContent, string(buf)); diff != "" {
t.Errorf("unexpected difference in lock file content: %s", diff)
}
})
}
// reattachedProviderForTest launches a provider process and returns a reattach config string
// that can be used as the value for the TF_REATTACH_PROVIDERS environment variable in tests.
// Cleanup of the provider process is handled internally.
func reattachedProviderForTest(t *testing.T, provider addrs.Provider, protocol int) string {
t.Helper()
if !slices.Contains([]int{5, 6}, protocol) {
t.Fatalf("test setup tried to create a provider using protocol version %d, which is unsupported. Choose between 5 and 6.", protocol)
}
reattachCh := make(chan *plugin.ReattachConfig)
closeCh := make(chan struct{})
server := &providerServer{
ProviderServer: grpcwrap.Provider6(simple.Provider()),
}
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(func() {
cancel()
<-closeCh
})
go plugin.Serve(&plugin.ServeConfig{
Logger: hclog.New(&hclog.LoggerOptions{
Name: "plugintest",
Level: hclog.Trace,
Output: io.Discard,
}),
Test: &plugin.ServeTestConfig{
Context: ctx,
ReattachConfigCh: reattachCh,
CloseCh: closeCh,
},
GRPCServer: plugin.DefaultGRPCServer,
VersionedPlugins: map[int]plugin.PluginSet{
6: {
"provider": &tfplugin.GRPCProviderPlugin{
GRPCProvider: func() proto.ProviderServer {
return server
},
},
},
},
})
config := <-reattachCh
if config == nil {
t.Fatalf("no reattach config received")
}
reattachStr, err := json.Marshal(map[string]reattachConfig{
provider.String(): {
Protocol: string(config.Protocol),
ProtocolVersion: 6,
Pid: config.Pid,
Test: true,
Addr: reattachConfigAddr{
Network: config.Addr.Network(),
String: config.Addr.String(),
},
},
})
if err != nil {
t.Fatal(err)
}
return string(reattachStr)
}

View file

@ -0,0 +1,208 @@
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"slices"
"testing"
"github.com/apparentlymart/go-versions/versions"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/depsfile"
"github.com/hashicorp/terraform/internal/getproviders"
"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
"github.com/hashicorp/terraform/internal/providercache"
)
// Test the impacts of dev_overrides and reattached/unmanaged providers on the provider installation process.
// The locks returned from EnsureProviderVersions are what's saved to the dependency lock file, so we are interested
// in how the pre-existing locks and how providers are overidden impacts the locks returned from that installation process.
func TestEnsureProviderVersions_devOverrideAndReattachedProviders(t *testing.T) {
providerSource := newMockProviderSource(t, map[string][]string{
"provider-a": {"1.0.0", "2.0.0", "3.0.0", "4.0.0"},
"provider-b": {"1.0.0", "2.0.0", "3.0.0", "4.0.0"},
"provider-c": {"1.0.0", "2.0.0", "3.0.0", "4.0.0"},
"provider-d": {"1.0.0", "2.0.0", "3.0.0", "4.0.0"},
})
providerA := addrs.NewDefaultProvider("provider-a")
providerB := addrs.NewDefaultProvider("provider-b")
providerC := addrs.NewDefaultProvider("provider-c")
providerD := addrs.NewDefaultProvider("provider-d")
// In all test cases the imagined config required providers A through D at specific versions.
reqs := providerreqs.Requirements{
providerA: providerreqs.MustParseVersionConstraints("1.0.0"),
providerB: providerreqs.MustParseVersionConstraints("2.0.0"),
providerC: providerreqs.MustParseVersionConstraints("3.0.0"),
providerD: providerreqs.MustParseVersionConstraints("4.0.0"),
}
// Some tests are installing providers for the first time, and prior locks include only A.
priorLocksJustA := depsfile.NewLocks()
priorLocksJustA.SetProvider(
providerA,
versions.MustParseVersion("1.0.0"),
providerreqs.MustParseVersionConstraints("1.0.0"),
nil, // no hashes needed for this test
)
// Other tests are performing an install after all providers (A-D) have already been added to the dependency lock file.
priorLocksABCD := depsfile.NewLocks()
priorLocksABCD.SetProvider(
providerA,
versions.MustParseVersion("1.0.0"),
providerreqs.MustParseVersionConstraints("1.0.0"),
nil, // no hashes needed for this test
)
priorLocksABCD.SetProvider(
providerB,
versions.MustParseVersion("2.0.0"),
providerreqs.MustParseVersionConstraints("2.0.0"),
nil, // no hashes needed for this test
)
priorLocksABCD.SetProvider(
providerC,
versions.MustParseVersion("3.0.0"),
providerreqs.MustParseVersionConstraints("3.0.0"),
nil, // no hashes needed for this test
)
priorLocksABCD.SetProvider(
providerD,
versions.MustParseVersion("4.0.0"),
providerreqs.MustParseVersionConstraints("4.0.0"),
nil, // no hashes needed for this test
)
cases := map[string]struct {
providerDevOverrides map[addrs.Provider]getproviders.PackageLocalDir
unmanagedProviders map[addrs.Provider]*plugin.ReattachConfig
priorLocks *depsfile.Locks
expectedProviderTypesInLocks []string
}{
"no overrides or unmanaged providers": {
providerDevOverrides: map[addrs.Provider]getproviders.PackageLocalDir{},
unmanagedProviders: map[addrs.Provider]*plugin.ReattachConfig{},
priorLocks: priorLocksJustA, // Prior locks contain only provider A.
expectedProviderTypesInLocks: []string{
// All required providers are installed as expected.
providerA.ForDisplay(),
providerB.ForDisplay(),
providerC.ForDisplay(),
providerD.ForDisplay(),
},
},
"reattachment present at first-time installation of provider": {
providerDevOverrides: map[addrs.Provider]getproviders.PackageLocalDir{},
unmanagedProviders: map[addrs.Provider]*plugin.ReattachConfig{
providerD: {
Protocol: "grpc",
ProtocolVersion: 6,
Pid: 1234567890,
Test: true,
Addr: nil,
},
},
priorLocks: priorLocksJustA, // Prior locks contain only provider A.
expectedProviderTypesInLocks: []string{
providerA.ForDisplay(),
providerB.ForDisplay(), // New
providerC.ForDisplay(), // New
// D is not installed due to being reattached/unmanaged
},
},
"reattachment present at subsequent installation of provider": {
providerDevOverrides: map[addrs.Provider]getproviders.PackageLocalDir{},
unmanagedProviders: map[addrs.Provider]*plugin.ReattachConfig{
providerD: {
Protocol: "grpc",
ProtocolVersion: 6,
Pid: 1234567890,
Test: true,
Addr: nil,
},
},
priorLocks: priorLocksABCD, // Prior locks include the provider that's being reattached/unmanaged.
expectedProviderTypesInLocks: []string{
providerA.ForDisplay(),
providerB.ForDisplay(),
providerC.ForDisplay(),
providerD.ForDisplay(), // Pre-existing lock for D is expected to be unaffected by use of reattachment/unmanaged.
},
},
"dev override present at first-time installation of provider": {
providerDevOverrides: map[addrs.Provider]getproviders.PackageLocalDir{
providerD: "/path/to/local/provider-d",
},
unmanagedProviders: map[addrs.Provider]*plugin.ReattachConfig{},
priorLocks: priorLocksJustA, // Prior locks contain only provider A.
expectedProviderTypesInLocks: []string{
providerA.ForDisplay(),
providerB.ForDisplay(),
providerC.ForDisplay(),
providerD.ForDisplay(), // D is installed despite being dev overridden
},
},
"dev override present at subsequent installation of provider": {
providerDevOverrides: map[addrs.Provider]getproviders.PackageLocalDir{
providerD: "/path/to/local/provider-d",
},
unmanagedProviders: map[addrs.Provider]*plugin.ReattachConfig{},
priorLocks: priorLocksABCD, // Prior locks include the provider that's being dev overridden.
expectedProviderTypesInLocks: []string{
providerA.ForDisplay(),
providerB.ForDisplay(),
providerC.ForDisplay(),
providerD.ForDisplay(), // Pre-existing lock for D is expected to be unaffected by use of dev_override.
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
// Temp dir needed because provider installation process writes to filesystem.
td := t.TempDir()
t.Chdir(td)
meta := Meta{
ProviderSource: providerSource,
ProviderDevOverrides: tc.providerDevOverrides,
UnmanagedProviders: tc.unmanagedProviders,
}
inst := meta.providerInstaller()
if inst == nil {
t.Fatal("expected installer, got nil")
}
// We cannot make assertions about internals of the installer resulting from (Meta).providerInstaller(),
// but we can make assertions on outputs from using the installer. Arguably this is more informative.
ctx := t.Context()
locks, err := inst.EnsureProviderVersions(ctx, tc.priorLocks, reqs, providercache.InstallNewProvidersOnly)
if err != nil {
t.Fatal(err)
}
if locks == nil {
t.Fatal("expected locks, got nil")
}
var gotProviderTypes []string
for addr := range locks.AllProviders() {
gotProviderTypes = append(gotProviderTypes, addr.ForDisplay())
}
slices.Sort(tc.expectedProviderTypesInLocks)
slices.Sort(gotProviderTypes)
if diff := cmp.Diff(tc.expectedProviderTypesInLocks, gotProviderTypes); diff != "" {
t.Errorf("unexpected difference in expected provider types in locks: %s", diff)
}
})
}
}