diff --git a/.changes/v1.13/BUG FIXES-20250524-035653.yaml b/.changes/v1.13/BUG FIXES-20250524-035653.yaml new file mode 100644 index 000000000000..47adaddbbcb2 --- /dev/null +++ b/.changes/v1.13/BUG FIXES-20250524-035653.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: suppress false-positive plugin_cache_dir error when -chdir is used +time: 2025-05-24T03:56:53.347808+09:00 +custom: + Issue: "36881" diff --git a/internal/command/cliconfig/cliconfig.go b/internal/command/cliconfig/cliconfig.go index e162fe44f9ee..f96bf71cc5b8 100644 --- a/internal/command/cliconfig/cliconfig.go +++ b/internal/command/cliconfig/cliconfig.go @@ -325,11 +325,17 @@ func (c *Config) Validate() tfdiags.Diagnostics { } if c.PluginCacheDir != "" { - _, err := os.Stat(c.PluginCacheDir) - if err != nil { - diags = diags.Append( - fmt.Errorf("The specified plugin cache dir %s cannot be opened: %s", c.PluginCacheDir, err), - ) + // Skip validation for relative paths here, as they may be intended + // to be resolved relative to a directory specified by -chdir, which + // is processed after config loading. Relative paths will be + // re-validated later in main.go after any -chdir option is processed. + if filepath.IsAbs(c.PluginCacheDir) { + _, err := os.Stat(c.PluginCacheDir) + if err != nil { + diags = diags.Append( + fmt.Errorf("The specified plugin cache dir %s cannot be opened: %s", c.PluginCacheDir, err), + ) + } } } diff --git a/internal/command/cliconfig/cliconfig_test.go b/internal/command/cliconfig/cliconfig_test.go index 87720fb7f0b6..2f0088dce332 100644 --- a/internal/command/cliconfig/cliconfig_test.go +++ b/internal/command/cliconfig/cliconfig_test.go @@ -357,12 +357,18 @@ func TestConfigValidate(t *testing.T) { }, 1, // no more than one provider_installation block allowed }, - "plugin_cache_dir does not exist": { + "plugin_cache_dir absolute path does not exist": { &Config{ - PluginCacheDir: "fake", + PluginCacheDir: "/absolute/fake/path", }, 1, // The specified plugin cache dir %s cannot be opened }, + "plugin_cache_dir relative path": { + &Config{ + PluginCacheDir: "../relative/fake/path", + }, + 0, // Relative paths are not validated in early validation phase + }, } for name, test := range tests { diff --git a/main.go b/main.go index cce3f5f998ad..c463211141b3 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/terraform/internal/httpclient" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/terminal" + "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/version" "github.com/mattn/go-shellwords" "github.com/mitchellh/colorstring" @@ -238,6 +239,27 @@ func realMain() int { Ui.Error(fmt.Sprintf("Error handling -chdir option: %s", err)) return 1 } + + // After changing the working directory with -chdir, we need to re-validate + // any relative plugin cache directory paths, since they are resolved relative + // to the current working directory. + if pluginCacheDir := config.PluginCacheDir; pluginCacheDir != "" && !filepath.IsAbs(pluginCacheDir) { + if _, err := os.Stat(pluginCacheDir); err != nil { + Ui.Error("There are some problems with the CLI configuration:") + earlyColor := &colorstring.Colorize{ + Colors: colorstring.DefaultColors, + Disable: true, + Reset: true, + } + diag := tfdiags.Sourceless( + tfdiags.Error, + "Invalid plugin cache directory", + fmt.Sprintf("The specified plugin cache dir %s cannot be opened: %s", pluginCacheDir, err), + ) + Ui.Error(format.Diagnostic(diag, nil, earlyColor, 78)) + Ui.Error("As a result of the above problems, Terraform may not behave as intended.\n\n") + } + } } // In tests, Commands may already be set to provide mock commands diff --git a/main_test.go b/main_test.go index addd07dfebaa..7ba210c3b7a5 100644 --- a/main_test.go +++ b/main_test.go @@ -6,10 +6,12 @@ package main import ( "fmt" "os" + "path/filepath" "reflect" "testing" "github.com/hashicorp/cli" + "github.com/hashicorp/terraform/internal/command/cliconfig" ) func TestMain_cliArgsFromEnv(t *testing.T) { @@ -329,3 +331,58 @@ func TestWarnOutput(t *testing.T) { t.Fatalf("unexpected stdout: %q\n", stdout) } } + +func TestPluginCacheDirWithChdir(t *testing.T) { + // Create a temporary directory structure for testing + tmpDir := t.TempDir() + subDir := filepath.Join(tmpDir, "subdir") + cacheDir := filepath.Join(tmpDir, "cache") + + if err := os.MkdirAll(subDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(cacheDir, 0755); err != nil { + t.Fatal(err) + } + + // Save original working directory + originalWd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + defer os.Chdir(originalWd) + + // Change to tmpDir + if err := os.Chdir(tmpDir); err != nil { + t.Fatal(err) + } + + // Test case 1: Relative path that exists + config := &cliconfig.Config{ + PluginCacheDir: "../cache", + } + + // Change to subdir (simulating -chdir) + if err := os.Chdir(subDir); err != nil { + t.Fatal(err) + } + + // Validate relative path + if pluginCacheDir := config.PluginCacheDir; pluginCacheDir != "" && !filepath.IsAbs(pluginCacheDir) { + if _, err := os.Stat(pluginCacheDir); err != nil { + t.Errorf("Expected relative plugin cache dir to be accessible after chdir, but got error: %v", err) + } + } + + // Test case 2: Relative path that doesn't exist + config2 := &cliconfig.Config{ + PluginCacheDir: "../nonexistent", + } + + // Validate non-existent relative path + if pluginCacheDir := config2.PluginCacheDir; pluginCacheDir != "" && !filepath.IsAbs(pluginCacheDir) { + if _, err := os.Stat(pluginCacheDir); err == nil { + t.Error("Expected non-existent relative plugin cache dir to fail validation") + } + } +}