Support the XDG Base Directory Specification (#1200)

Signed-off-by: Mario Valderrama <mario.valderrama@ionos.com>
This commit is contained in:
Mario Valderrama 2024-01-31 11:04:09 +01:00 committed by GitHub
parent f96cb50405
commit e84b2f7f95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 218 additions and 19 deletions

View File

@ -16,6 +16,7 @@ ENHANCEMENTS:
* Added "base64gunzip" function. ([$800](https://github.com/opentofu/opentofu/issues/800))
* Added "cidrcontains" function. ([$366](https://github.com/opentofu/opentofu/issues/366))
* Allow test run blocks to reference previous run block's module outputs ([#1129](https://github.com/opentofu/opentofu/pull/1129))
* Support the XDG Base Directory Specification ([#1200](https://github.com/opentofu/opentofu/pull/1200))
BUG FIXES:
* `tofu test` resources cleanup at the end of tests changed to use simple reverse run block order. ([#1043](https://github.com/opentofu/opentofu/pull/1043))

View File

@ -501,7 +501,7 @@ func extractChdirOption(args []string) (string, []string, error) {
}
// Creates the the configuration directory.
// `configDir` should refer to `~/.terraform.d` or its equivalent
// `configDir` should refer to `~/.terraform.d`, `$XDG_CONFIG_HOME/opentofu` or its equivalent
// on non-UNIX platforms.
func mkConfigDir(configDir string) error {
err := os.Mkdir(configDir, os.ModePerm)

View File

@ -20,14 +20,16 @@ import (
// older versions where both satisfy the provider version constraints.
func globalPluginDirs() []string {
var ret []string
// Look in ~/.terraform.d/plugins/ , or its equivalent on non-UNIX
dir, err := cliconfig.ConfigDir()
// Look in ~/.terraform.d/plugins/, $XDG_DATA_HOME/opentofu/plugins, or its equivalent on non-UNIX platforms
dirs, err := cliconfig.DataDirs()
if err != nil {
log.Printf("[ERROR] Error finding global config directory: %s", err)
log.Printf("[ERROR] Error finding global plugin directories: %s", err)
} else {
machineDir := fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)
ret = append(ret, filepath.Join(dir, "plugins"))
ret = append(ret, filepath.Join(dir, "plugins", machineDir))
for _, dir := range dirs {
ret = append(ret, filepath.Join(dir, "plugins"))
ret = append(ret, filepath.Join(dir, "plugins", machineDir))
}
}
return ret

View File

@ -96,7 +96,8 @@ func implicitProviderSource(services *disco.Disco) getproviders.Source {
// way to include them in bundles uploaded to Terraform Cloud, where
// there has historically otherwise been no way to use custom providers.
// - The "plugins" subdirectory of the CLI config search directory.
// (thats ~/.terraform.d/plugins on Unix systems, equivalents elsewhere)
// (thats ~/.terraform.d/plugins or $XDG_DATA_HOME/opentofu/plugins
// on Unix systems, equivalents elsewhere)
// - The "plugins" subdirectory of any platform-specific search paths,
// following e.g. the XDG base directory specification on Unix systems,
// Apple's guidelines on OS X, and "known folders" on Windows.
@ -144,9 +145,11 @@ func implicitProviderSource(services *disco.Disco) getproviders.Source {
}
addLocalDir("terraform.d/plugins") // our "vendor" directory
cliConfigDir, err := cliconfig.ConfigDir()
cliDataDirs, err := cliconfig.DataDirs()
if err == nil {
addLocalDir(filepath.Join(cliConfigDir, "plugins"))
for _, cliDataDir := range cliDataDirs {
addLocalDir(filepath.Join(cliDataDir, "plugins"))
}
}
// This "userdirs" library implements an appropriate user-specific and

View File

@ -96,6 +96,11 @@ func ConfigDir() (string, error) {
return configDir()
}
// DataDirs returns the data directories for OpenTofu.
func DataDirs() ([]string, error) {
return dataDirs()
}
// LoadConfig reads the CLI configuration from the various filesystem locations
// and from the environment, returning a merged configuration along with any
// diagnostics (errors and warnings) encountered along the way.

View File

@ -22,6 +22,11 @@ func configFile() (string, error) {
newConfigFile := filepath.Join(dir, ".tofurc")
legacyConfigFile := filepath.Join(dir, ".terraformrc")
if xdgDir := os.Getenv("XDG_CONFIG_HOME"); xdgDir != "" && !pathExists(legacyConfigFile) && !pathExists(newConfigFile) {
// a fresh install should not use terraform naming
return filepath.Join(xdgDir, "opentofu", "tofurc"), nil
}
return getNewOrLegacyPath(newConfigFile, legacyConfigFile)
}
@ -31,7 +36,26 @@ func configDir() (string, error) {
return "", err
}
return filepath.Join(dir, ".terraform.d"), nil
configDir := filepath.Join(dir, ".terraform.d")
if xdgDir := os.Getenv("XDG_CONFIG_HOME"); !pathExists(configDir) && xdgDir != "" {
configDir = filepath.Join(xdgDir, "opentofu")
}
return configDir, nil
}
func dataDirs() ([]string, error) {
dir, err := homeDir()
if err != nil {
return nil, err
}
dirs := []string{filepath.Join(dir, ".terraform.d")}
if xdgDir := os.Getenv("XDG_DATA_HOME"); xdgDir != "" {
dirs = append(dirs, filepath.Join(xdgDir, "opentofu"))
}
return dirs, nil
}
func homeDir() (string, error) {
@ -40,7 +64,7 @@ func homeDir() (string, error) {
// FIXME: homeDir gets called from globalPluginDirs during init, before
// the logging is set up. We should move meta initializtion outside of
// init, but in the meantime we just need to silence this output.
//log.Printf("[DEBUG] Detected home directory from env var: %s", home)
// log.Printf("[DEBUG] Detected home directory from env var: %s", home)
return home, nil
}
@ -57,3 +81,8 @@ func homeDir() (string, error) {
return user.HomeDir, nil
}
func pathExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}

View File

@ -0,0 +1,151 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build !windows
// +build !windows
package cliconfig
import (
"os"
"path/filepath"
"slices"
"testing"
)
func TestConfigFileConfigDir(t *testing.T) {
homeDir := filepath.Join(t.TempDir(), "home")
tests := []struct {
name string
xdgConfigHome string
files []string
testFunc func() (string, error)
expect string
}{
{
name: "configFile: use home tofurc",
testFunc: configFile,
files: []string{filepath.Join(homeDir, ".tofurc")},
expect: filepath.Join(homeDir, ".tofurc"),
},
{
name: "configFile: use home terraformrc",
testFunc: configFile,
files: []string{filepath.Join(homeDir, ".terraformrc")},
expect: filepath.Join(homeDir, ".terraformrc"),
},
{
name: "configFile: use default fallback",
testFunc: configFile,
expect: filepath.Join(homeDir, ".tofurc"),
},
{
name: "configFile: use XDG tofurc",
testFunc: configFile,
xdgConfigHome: filepath.Join(homeDir, "xdg"),
expect: filepath.Join(homeDir, "xdg", "opentofu", "tofurc"),
},
{
name: "configFile: prefer home tofurc",
testFunc: configFile,
xdgConfigHome: filepath.Join(homeDir, "xdg"),
files: []string{filepath.Join(homeDir, ".tofurc")},
expect: filepath.Join(homeDir, ".tofurc"),
},
{
name: "configFile: prefer home terraformrc",
testFunc: configFile,
xdgConfigHome: filepath.Join(homeDir, "xdg"),
files: []string{filepath.Join(homeDir, ".terraformrc")},
expect: filepath.Join(homeDir, ".terraformrc"),
},
{
name: "configDir: use .terraform.d default",
testFunc: configDir,
expect: filepath.Join(homeDir, ".terraform.d"),
},
{
name: "configDir: prefer .terraform.d",
testFunc: configDir,
xdgConfigHome: filepath.Join(homeDir, "xdg"),
files: []string{filepath.Join(homeDir, ".terraform.d", "placeholder")},
expect: filepath.Join(homeDir, ".terraform.d"),
},
{
name: "configDir: use XDG value",
testFunc: configDir,
xdgConfigHome: filepath.Join(homeDir, "xdg"),
expect: filepath.Join(homeDir, "xdg", "opentofu"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Setenv("HOME", homeDir)
t.Setenv("XDG_CONFIG_HOME", test.xdgConfigHome)
for _, f := range test.files {
createFile(t, f)
}
file, err := test.testFunc()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if test.expect != file {
t.Fatalf("expected %q, but got %q", test.expect, file)
}
})
}
}
func TestDataDirs(t *testing.T) {
homeDir := filepath.Join(t.TempDir(), "home")
tests := []struct {
name string
xdgDataHome string
expect []string
}{
{
name: "use XDG data dir",
xdgDataHome: filepath.Join(homeDir, "xdg"),
expect: []string{
filepath.Join(homeDir, ".terraform.d"),
filepath.Join(homeDir, "xdg", "opentofu"),
},
},
{
name: "use default",
expect: []string{
filepath.Join(homeDir, ".terraform.d"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Setenv("HOME", homeDir)
t.Setenv("XDG_DATA_HOME", test.xdgDataHome)
dirs, err := dataDirs()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !slices.Equal(test.expect, dirs) {
t.Fatalf("expected %+v, but got %+v", test.expect, dirs)
}
})
}
}
func createFile(t *testing.T, path string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, nil, 0o600); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.RemoveAll(filepath.Dir(path)) })
}

View File

@ -40,6 +40,14 @@ func configDir() (string, error) {
return filepath.Join(dir, "terraform.d"), nil
}
func dataDirs() ([]string, error) {
dir, err := configDir()
if err != nil {
return nil, err
}
return []string{dir}, nil
}
func homeDir() (string, error) {
b := make([]uint16, syscall.MAX_PATH)

View File

@ -24,9 +24,12 @@ on the host operating system:
If both `terraform.rc` and `tofu.rc` files exists, the later would take precedence.
* On all other systems, the file must be named `.tofurc` (note
the leading period) and placed directly in the home directory
of the relevant user.
of the relevant user or be named `tofurc` and placed in a valid
[XDG Base Directory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
config directory such as `$XDG_CONFIG_HOME/opentofu`.
The `.terraformrc` is supported for backward-compatability purposes.
If both `.terraformrc` and `.tofurc` files exists, the later would take precedence.
If both `.terraformrc` and `.tofurc` files exists, the latter would take precedence.
When using an XDG config directory `.terraformrc` and `terraformrc` are ignored.
On Windows, beware of Windows Explorer's default behavior of hiding filename
extensions. OpenTofu will not recognize a file named `tofuc.rc.txt` as a
@ -275,12 +278,9 @@ the operating system where you are running OpenTofu:
`~/Library/Application Support/io.terraform/plugins`, and
`/Library/Application Support/io.terraform/plugins`
* **Linux and other Unix-like systems**:`$HOME/.terraform.d/plugins` and
`terraform/plugins` located within a valid
`opentofu/plugins` located within a valid
[XDG Base Directory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
data directory such as `$XDG_DATA_HOME/terraform/plugins`.
Without any XDG environment variables set, OpenTofu will use
`~/.local/share/terraform/plugins`,
`/usr/local/share/terraform/plugins`, and `/usr/share/terraform/plugins`.
data directory such as `$XDG_DATA_HOME/opentofu/plugins`.
If a `terraform.d/plugins` directory exists in the current working directory
then OpenTofu will also include that directory, regardless of your operating

View File

@ -195,7 +195,7 @@ provisioners must connect to the remote system using SSH or WinRM.
You must include [a `connection` block](/docs/language/resources/provisioners/connection) so that OpenTofu knows how to communicate with the server.
OpenTofu includes several built-in provisioners. You can also use third-party provisioners as plugins, by placing them
in `%APPDATA%\terraform.d\plugins`, `~/.terraform.d/plugins`, or the same
in `%APPDATA%\terraform.d\plugins`, `~/.terraform.d/plugins`, `$XDG_DATA_HOME/opentofu/plugins`, or the same
directory where the OpenTofu binary is installed. However, we do not recommend
using any provisioners except the built-in `file`, `local-exec`, and
`remote-exec` provisioners.