Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.15/ENHANCEMENTS-20260108-121031.yaml
Original file line number Diff line number Diff line change
@@ -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"
29 changes: 27 additions & 2 deletions internal/command/cliconfig/config_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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
}
291 changes: 291 additions & 0 deletions internal/command/cliconfig/config_unix_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
})
}