diff --git a/internal/command/e2etest/provider_plugin_test.go b/internal/command/e2etest/provider_plugin_test.go index e0cf9ca8a5..b23e6deddf 100644 --- a/internal/command/e2etest/provider_plugin_test.go +++ b/internal/command/e2etest/provider_plugin_test.go @@ -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) +} diff --git a/internal/command/meta_providers_test.go b/internal/command/meta_providers_test.go new file mode 100644 index 0000000000..de04620a07 --- /dev/null +++ b/internal/command/meta_providers_test.go @@ -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) + } + }) + } +}