From 7ca8c8f2192bc2dc150c0132c5ef118d2ba18c92 Mon Sep 17 00:00:00 2001 From: Alexander Bruyako Date: Thu, 8 Jan 2026 11:07:21 +0300 Subject: [PATCH 1/2] feature: support XDG Base Directory Specification on Unix Fall back to traditional paths if XDG config directory is not present. Add comprehensive tests covering XDG and legacy configurations. --- internal/command/cliconfig/config_unix.go | 29 +- .../command/cliconfig/config_unix_test.go | 291 ++++++++++++++++++ 2 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 internal/command/cliconfig/config_unix_test.go diff --git a/internal/command/cliconfig/config_unix.go b/internal/command/cliconfig/config_unix.go index 71bb390f2af2..93f5863932c9 100644 --- a/internal/command/cliconfig/config_unix.go +++ b/internal/command/cliconfig/config_unix.go @@ -19,7 +19,12 @@ func configFile() (string, error) { return "", err } - return filepath.Join(dir, ".terraformrc"), nil + xdgdir, err := configDirXDG() + if err != nil { + return filepath.Join(dir, ".terraformrc"), nil + } + + return filepath.Join(xdgdir, "terraformrc"), nil } func configDir() (string, error) { @@ -28,7 +33,12 @@ func configDir() (string, error) { return "", err } - return filepath.Join(dir, ".terraform.d"), nil + xdgdir, err := configDirXDG() + if err != nil { + return filepath.Join(dir, ".terraform.d"), nil + } + + return filepath.Join(xdgdir, "terraform.d"), nil } func homeDir() (string, error) { @@ -54,3 +64,18 @@ func homeDir() (string, error) { return user.HomeDir, nil } + +func configDirXDG() (string, error) { + configDir, err := os.UserConfigDir() + if err != nil { + return "", err + } + + terraformConfigDir := filepath.Join(configDir, "terraform") + + if _, err := os.Stat(terraformConfigDir); err != nil { + return "", err + } + + return terraformConfigDir, nil +} diff --git a/internal/command/cliconfig/config_unix_test.go b/internal/command/cliconfig/config_unix_test.go new file mode 100644 index 000000000000..daa85544187d --- /dev/null +++ b/internal/command/cliconfig/config_unix_test.go @@ -0,0 +1,291 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !windows +// +build !windows + +package cliconfig + +import ( + "os" + "path/filepath" + "testing" +) + +func Test_configDir(t *testing.T) { + const ( + defaultDirPattern = "go_test_terraform_configdir" + + envHome = "HOME" + envXdgConfig = "XDG_CONFIG_HOME" + ) + + t.Run("empty home of user", func(t *testing.T) { + tmpdir, err := os.MkdirTemp("", defaultDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + t.Setenv(envHome, tmpdir) + got, err := configDir() + if err != nil { + t.Fatal(err) + } + + want := filepath.Join(tmpdir, ".terraform.d") + + if got != want { + t.Errorf("configDir() = %v, want %v", got, want) + } + }) + + t.Run("has xdg env var, but no actual dir", func(t *testing.T) { + tmpdir, err := os.MkdirTemp("", defaultDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + t.Setenv(envHome, tmpdir) + t.Setenv(envXdgConfig, filepath.Join(tmpdir, ".config")) + + got, err := configDir() + if err != nil { + t.Fatal(err) + } + + want := filepath.Join(tmpdir, ".terraform.d") + + if got != want { + t.Errorf("configDir() = %v, want %v", got, want) + } + }) + + t.Run("terraform config dir exists in home directory", func(t *testing.T) { + tmpdir, err := os.MkdirTemp("", defaultDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + terraformConfigDir := filepath.Join(tmpdir, ".terraform.d") + err = os.MkdirAll(terraformConfigDir, 0755) + if err != nil { + t.Fatal(err) + } + + t.Setenv(envHome, tmpdir) + + got, err := configDir() + if err != nil { + t.Fatal(err) + } + + want := terraformConfigDir + if got != want { + t.Errorf("configDir() = %v, want %v", got, want) + } + }) + + t.Run("has xdg config dir with terraform config", func(t *testing.T) { + tmpdir, err := os.MkdirTemp("", defaultDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + xdgConfigDir := filepath.Join(tmpdir, ".config") + terraformConfigDir := filepath.Join(xdgConfigDir, "terraform", "terraform.d") + if err := os.MkdirAll(terraformConfigDir, 0755); err != nil { + t.Fatal(err) + } + + t.Setenv(envHome, tmpdir) + t.Setenv(envXdgConfig, xdgConfigDir) + + got, err := configDir() + if err != nil { + t.Fatal(err) + } + + want := terraformConfigDir + + if got != want { + t.Errorf("configDir() = %v, want %v", got, want) + } + }) +} + +func Test_configFile(t *testing.T) { + const ( + defaultDirPattern = "go_test_terraform_configfile" + + envHome = "HOME" + envXdgConfig = "XDG_CONFIG_HOME" + ) + + t.Run("empty home of user", func(t *testing.T) { + tmpdir, err := os.MkdirTemp("", defaultDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + t.Setenv(envHome, tmpdir) + got, err := configFile() + if err != nil { + t.Fatal(err) + } + + want := filepath.Join(tmpdir, ".terraformrc") + + if got != want { + t.Errorf("configFile() = %v, want %v", got, want) + } + }) + + t.Run("has xdg env var, but no actual dir", func(t *testing.T) { + tmpdir, err := os.MkdirTemp("", defaultDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + t.Setenv(envHome, tmpdir) + t.Setenv(envXdgConfig, filepath.Join(tmpdir, ".config")) + + got, err := configFile() + if err != nil { + t.Fatal(err) + } + + want := filepath.Join(tmpdir, ".terraformrc") + + if got != want { + t.Errorf("configFile() = %v, want %v", got, want) + } + }) + + t.Run("terraform config file exists in home directory", func(t *testing.T) { + tmpdir, err := os.MkdirTemp("", defaultDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + terraformConfigDir := filepath.Join(tmpdir, ".terraformrc") + f, err := os.Create(terraformConfigDir) + if err != nil { + t.Fatal(err) + } + f.Close() + + t.Setenv(envHome, tmpdir) + + got, err := configFile() + if err != nil { + t.Fatal(err) + } + + want := terraformConfigDir + if got != want { + t.Errorf("configFile() = %v, want %v", got, want) + } + }) + + t.Run("has xdg config file with terraform config", func(t *testing.T) { + tmpdir, err := os.MkdirTemp("", defaultDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + xdgConfigDir := filepath.Join(tmpdir, ".config") + terraformConfigDir := filepath.Join(xdgConfigDir, "terraform") + terraformConfigFile := filepath.Join(terraformConfigDir, "terraformrc") + + if err := os.MkdirAll(terraformConfigDir, 0755); err != nil { + t.Fatal(err) + } + + f, err := os.Create(terraformConfigFile) + if err != nil { + t.Fatal(err) + } + f.Close() + + t.Setenv(envHome, tmpdir) + t.Setenv(envXdgConfig, xdgConfigDir) + + got, err := configFile() + if err != nil { + t.Fatal(err) + } + + want := terraformConfigFile + + if got != want { + t.Errorf("configFile() = %v, want %v", got, want) + } + }) +} + +func Test_configDirXDG(t *testing.T) { + const ( + defaultDirPattern = "go_test_terraform_configdir_xdg" + ) + + t.Run("terraform config dir exists", func(t *testing.T) { + tmpdir, err := os.MkdirTemp("", defaultDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + // Create the expected terraform config directory structure + terraformConfigDir := filepath.Join(tmpdir, "terraform") + if err := os.MkdirAll(terraformConfigDir, 0755); err != nil { + t.Fatal(err) + } + + t.Setenv("XDG_CONFIG_HOME", tmpdir) + + got, err := configDirXDG() + if err != nil { + t.Fatal(err) + } + + want := terraformConfigDir + if got != want { + t.Errorf("configDirXDG() = %v, want %v", got, want) + } + }) + + t.Run("terraform config dir does not exist", func(t *testing.T) { + tmpdir, err := os.MkdirTemp("", defaultDirPattern) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + // Don't create the expected terraform config directory structure, to get expected error + + t.Setenv("XDG_CONFIG_HOME", tmpdir) + + _, err = configDirXDG() + if err == nil { + t.Error("configDirXDG() expected error when terraform config dir does not exist, got nil") + } + }) + + t.Run("no HOME or XDG_CONFIG_HOME defined", func(t *testing.T) { + t.Setenv("HOME", "") + t.Setenv("XDG_CONFIG_HOME", "") + + _, err := configDirXDG() + if err == nil { + t.Error("configDirXDG() expected error when HOME or XDG_CONFIG_HOME not defined, got nil") + } + }) +} From 62f6aaa9c6a9a954ad6b1d65703aeb186ba817c1 Mon Sep 17 00:00:00 2001 From: Alexander Bruyako Date: Thu, 8 Jan 2026 12:15:23 +0300 Subject: [PATCH 2/2] feature: updates changes files about support XDG Base Directory Specification on Unix --- .changes/v1.15/ENHANCEMENTS-20260108-121031.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/v1.15/ENHANCEMENTS-20260108-121031.yaml diff --git a/.changes/v1.15/ENHANCEMENTS-20260108-121031.yaml b/.changes/v1.15/ENHANCEMENTS-20260108-121031.yaml new file mode 100644 index 000000000000..0b7df5f704bc --- /dev/null +++ b/.changes/v1.15/ENHANCEMENTS-20260108-121031.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: "added optional support for XDG Base Directory Specification on Unix systems" +time: 2026-01-08T12:10:32.721437+03:00 +custom: + Issue: "15389"