fix: resolve local mirror detection with -chdir option (#3072)

Signed-off-by: Babur Ayanlar <babur.ayanlar@ableton.com>
This commit is contained in:
baa-ableton
2025-08-13 11:13:00 +02:00
committed by GitHub
parent 6787734a9a
commit 917adc61b0
3 changed files with 183 additions and 26 deletions

View File

@@ -199,21 +199,12 @@ func realMain() int {
modulePkgFetcher := remoteModulePackageFetcher(ctx, config.OCICredentialsPolicy)
providerSrc, diags := providerSource(ctx, config.ProviderInstallation, services, config.OCICredentialsPolicy)
if len(diags) > 0 {
Ui.Error("There are some problems with the provider_installation configuration:")
for _, diag := range diags {
earlyColor := &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true, // Disable color to be conservative until we know better
Reset: true,
}
Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78))
}
if diags.HasErrors() {
Ui.Error("As a result of the above problems, OpenTofu's provider installer may not behave as intended.\n\n")
// We continue to run anyway, because most commands don't do provider installation.
}
// Get the original working directory before any -chdir processing
originalWd, err := os.Getwd()
if err != nil {
// It would be very strange to end up here
Ui.Error(fmt.Sprintf("Failed to determine current working directory: %s", err))
return 1
}
providerDevOverrides := providerDevOverrides(config.ProviderInstallation)
@@ -233,13 +224,6 @@ func realMain() int {
binName := filepath.Base(os.Args[0])
args := os.Args[1:]
originalWd, err := os.Getwd()
if err != nil {
// It would be very strange to end up here
Ui.Error(fmt.Sprintf("Failed to determine current working directory: %s", err))
return 1
}
// The arguments can begin with a -chdir option to ask OpenTofu to switch
// to a different working directory for the rest of its work. If that
// option is present then extractChdirOption returns a trimmed args with that option removed.
@@ -256,6 +240,23 @@ func realMain() int {
}
}
providerSrc, diags := providerSource(ctx, config.ProviderInstallation, services, config.OCICredentialsPolicy, originalWd)
if len(diags) > 0 {
Ui.Error("There are some problems with the provider_installation configuration:")
for _, diag := range diags {
earlyColor := &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true, // Disable color to be conservative until we know better
Reset: true,
}
Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78))
}
if diags.HasErrors() {
Ui.Error("As a result of the above problems, OpenTofu's provider installer may not behave as intended.\n\n")
// We continue to run anyway, because most commands don't do provider installation.
}
}
// In tests, Commands may already be set to provide mock commands
if commands == nil {
// Commands get to hold on to the original working directory here,

View File

@@ -26,12 +26,12 @@ import (
// CLI configuration and some default search locations. This will be the
// provider source used for provider installation in the "tofu init"
// command, unless overridden by the special -plugin-dir option.
func providerSource(ctx context.Context, configs []*cliconfig.ProviderInstallation, services *disco.Disco, getOCICredsPolicy ociCredsPolicyBuilder) (getproviders.Source, tfdiags.Diagnostics) {
func providerSource(ctx context.Context, configs []*cliconfig.ProviderInstallation, services *disco.Disco, getOCICredsPolicy ociCredsPolicyBuilder, originalWorkingDir string) (getproviders.Source, tfdiags.Diagnostics) {
if len(configs) == 0 {
// If there's no explicit installation configuration then we'll build
// up an implicit one with direct registry installation along with
// some automatically-selected local filesystem mirrors.
return implicitProviderSource(ctx, services), nil
return implicitProviderSource(ctx, services, originalWorkingDir), nil
}
// There should only be zero or one configurations, which is checked by
@@ -92,12 +92,14 @@ func explicitProviderSource(ctx context.Context, config *cliconfig.ProviderInsta
// one version available in a local directory are implicitly excluded from
// direct installation, as if the user had listed them explicitly in the
// "exclude" argument in the direct provider source in the CLI config.
func implicitProviderSource(ctx context.Context, services *disco.Disco) getproviders.Source {
func implicitProviderSource(ctx context.Context, services *disco.Disco, originalWorkingDir string) getproviders.Source {
// The local search directories we use for implicit configuration are:
// - The "terraform.d/plugins" directory in the current working directory,
// which we've historically documented as a place to put plugins as a
// way to include them in bundles uploaded to Terraform Cloud, where
// there has historically otherwise been no way to use custom providers.
// When using -chdir, this directory is checked in the launch directory
// (original working directory), not the directory specified with -chdir.
// - The "plugins" subdirectory of the CLI config search directory.
// (that's ~/.terraform.d/plugins or $XDG_DATA_HOME/opentofu/plugins
// on Unix systems, equivalents elsewhere)
@@ -147,7 +149,9 @@ func implicitProviderSource(ctx context.Context, services *disco.Disco) getprovi
}
}
addLocalDir("terraform.d/plugins") // our "vendor" directory
// Check and add the "terraform.d/plugins" directory in the original working directory
addLocalDir(filepath.Join(originalWorkingDir, "terraform.d/plugins"))
cliDataDirs, err := cliconfig.DataDirs()
if err == nil {
for _, cliDataDir := range cliDataDirs {

View File

@@ -0,0 +1,152 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package main
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/command/cliconfig"
"github.com/opentofu/opentofu/internal/command/cliconfig/ociauthconfig"
"github.com/opentofu/svchost/disco"
)
func TestProviderSource(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T) (string, string) // returns (originalDir, overrideDir)
expectedProvider string
}{
{
name: "without overrideWd - should use terraform.d/plugins in original working directory",
setupFunc: func(t *testing.T) (string, string) {
// Create a temporary directory to conduct the test in
tempDir := t.TempDir()
// Create terraform.d/plugins in the testing directory
pluginsDir := filepath.Join(tempDir, "terraform.d", "plugins")
err := os.MkdirAll(pluginsDir, 0755)
if err != nil {
t.Fatalf("Failed to create plugins directory: %v", err)
}
// Create a mock provider in the plugins directory
providerDir := filepath.Join(pluginsDir, "registry.opentofu.org", "hashicorp", "test-provider", "1.0.0", "linux_amd64")
err = os.MkdirAll(providerDir, 0755)
if err != nil {
t.Fatalf("Failed to create provider directory: %v", err)
}
// Create a mock provider binary
providerBinary := filepath.Join(providerDir, "terraform-provider-test-provider")
err = os.WriteFile(providerBinary, []byte("mock provider binary"), 0755)
if err != nil {
t.Fatalf("Failed to create mock provider binary: %v", err)
}
return tempDir, ""
},
expectedProvider: "hashicorp/test-provider",
},
{
name: "with overrideWd - should still use terraform.d/plugins in original working directory",
setupFunc: func(t *testing.T) (string, string) {
// Create a temporary directory to conduct the test in
tempDir := t.TempDir()
// Create terraform.d/plugins in the testing directory
pluginsDir := filepath.Join(tempDir, "terraform.d", "plugins")
err := os.MkdirAll(pluginsDir, 0755)
if err != nil {
t.Fatalf("Failed to create plugins directory: %v", err)
}
// Create a mock provider in the plugins directory
providerDir := filepath.Join(pluginsDir, "registry.opentofu.org", "hashicorp", "test-provider", "1.0.0", "linux_amd64")
err = os.MkdirAll(providerDir, 0755)
if err != nil {
t.Fatalf("Failed to create provider directory: %v", err)
}
// Create a mock provider binary
providerBinary := filepath.Join(providerDir, "terraform-provider-test-provider")
err = os.WriteFile(providerBinary, []byte("mock provider binary"), 0755)
if err != nil {
t.Fatalf("Failed to create mock provider binary: %v", err)
}
// Create a temporary directory for the override working directory
overrideDir := filepath.Join(tempDir, "override")
err = os.MkdirAll(overrideDir, 0755)
if err != nil {
t.Fatalf("Failed to create override directory: %v", err)
}
return tempDir, overrideDir
},
expectedProvider: "hashicorp/test-provider",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup test environment
originalWorkingDir, overrideWd := tt.setupFunc(t)
err := os.Chdir(originalWorkingDir)
if err != nil {
t.Fatalf("Failed to change to original working directory: %v", err)
}
// If we have an override directory, change to it (simulating -chdir behavior)
if overrideWd != "" {
err := os.Chdir(overrideWd)
if err != nil {
t.Fatalf("Failed to change to override directory: %v", err)
}
}
// Create a mock disco service
services := disco.New()
// Create a mock OCI credentials policy builder
ociCredsPolicy := func(ctx context.Context) (ociauthconfig.CredentialsConfigs, error) {
return ociauthconfig.CredentialsConfigs{}, nil
}
// Call the function under test
source, diags := providerSource(context.Background(), []*cliconfig.ProviderInstallation{}, services, ociCredsPolicy, originalWorkingDir)
// Verify no diagnostics were returned
if len(diags) > 0 {
t.Fatalf("Expected no diagnostics, got: %v", diags)
}
// Verify the source is not nil
if source == nil {
t.Fatal("Expected source to be non-nil")
}
// Try to get available versions for a test provider
// This will help verify that the local directories are properly configured
provider := addrs.MustParseProviderSourceString(tt.expectedProvider)
versions, _, err := source.AvailableVersions(context.Background(), provider)
if err != nil {
// If available provider versions could not be determined, something went wrong with mock provider setup
t.Fatalf("AvailableVersions failed (expected for test): %v", err)
}
t.Logf("Source created successfully for test case: %s", tt.name)
t.Logf("Provider: %v", provider)
if versions != nil {
t.Logf("Available versions: %v", versions)
}
})
}
}