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 "base64gunzip" function. ([$800](https://github.com/opentofu/opentofu/issues/800))
* Added "cidrcontains" function. ([$366](https://github.com/opentofu/opentofu/issues/366)) * 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)) * 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: 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)) * `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. // 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. // on non-UNIX platforms.
func mkConfigDir(configDir string) error { func mkConfigDir(configDir string) error {
err := os.Mkdir(configDir, os.ModePerm) err := os.Mkdir(configDir, os.ModePerm)

View File

@ -20,14 +20,16 @@ import (
// older versions where both satisfy the provider version constraints. // older versions where both satisfy the provider version constraints.
func globalPluginDirs() []string { func globalPluginDirs() []string {
var ret []string var ret []string
// Look in ~/.terraform.d/plugins/ , or its equivalent on non-UNIX // Look in ~/.terraform.d/plugins/, $XDG_DATA_HOME/opentofu/plugins, or its equivalent on non-UNIX platforms
dir, err := cliconfig.ConfigDir() dirs, err := cliconfig.DataDirs()
if err != nil { if err != nil {
log.Printf("[ERROR] Error finding global config directory: %s", err) log.Printf("[ERROR] Error finding global plugin directories: %s", err)
} else { } else {
machineDir := fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH) machineDir := fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)
ret = append(ret, filepath.Join(dir, "plugins")) for _, dir := range dirs {
ret = append(ret, filepath.Join(dir, "plugins", machineDir)) ret = append(ret, filepath.Join(dir, "plugins"))
ret = append(ret, filepath.Join(dir, "plugins", machineDir))
}
} }
return ret 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 // way to include them in bundles uploaded to Terraform Cloud, where
// there has historically otherwise been no way to use custom providers. // there has historically otherwise been no way to use custom providers.
// - The "plugins" subdirectory of the CLI config search directory. // - 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, // - The "plugins" subdirectory of any platform-specific search paths,
// following e.g. the XDG base directory specification on Unix systems, // following e.g. the XDG base directory specification on Unix systems,
// Apple's guidelines on OS X, and "known folders" on Windows. // 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 addLocalDir("terraform.d/plugins") // our "vendor" directory
cliConfigDir, err := cliconfig.ConfigDir() cliDataDirs, err := cliconfig.DataDirs()
if err == nil { 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 // This "userdirs" library implements an appropriate user-specific and

View File

@ -96,6 +96,11 @@ func ConfigDir() (string, error) {
return configDir() 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 // LoadConfig reads the CLI configuration from the various filesystem locations
// and from the environment, returning a merged configuration along with any // and from the environment, returning a merged configuration along with any
// diagnostics (errors and warnings) encountered along the way. // diagnostics (errors and warnings) encountered along the way.

View File

@ -22,6 +22,11 @@ func configFile() (string, error) {
newConfigFile := filepath.Join(dir, ".tofurc") newConfigFile := filepath.Join(dir, ".tofurc")
legacyConfigFile := filepath.Join(dir, ".terraformrc") 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) return getNewOrLegacyPath(newConfigFile, legacyConfigFile)
} }
@ -31,7 +36,26 @@ func configDir() (string, error) {
return "", err 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) { func homeDir() (string, error) {
@ -40,7 +64,7 @@ func homeDir() (string, error) {
// FIXME: homeDir gets called from globalPluginDirs during init, before // FIXME: homeDir gets called from globalPluginDirs during init, before
// the logging is set up. We should move meta initializtion outside of // the logging is set up. We should move meta initializtion outside of
// init, but in the meantime we just need to silence this output. // 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 return home, nil
} }
@ -57,3 +81,8 @@ func homeDir() (string, error) {
return user.HomeDir, nil 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 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) { func homeDir() (string, error) {
b := make([]uint16, syscall.MAX_PATH) 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. 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 * On all other systems, the file must be named `.tofurc` (note
the leading period) and placed directly in the home directory 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. 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 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 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`, and
`/Library/Application Support/io.terraform/plugins` `/Library/Application Support/io.terraform/plugins`
* **Linux and other Unix-like systems**:`$HOME/.terraform.d/plugins` and * **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) [XDG Base Directory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html)
data directory such as `$XDG_DATA_HOME/terraform/plugins`. data directory such as `$XDG_DATA_HOME/opentofu/plugins`.
Without any XDG environment variables set, OpenTofu will use
`~/.local/share/terraform/plugins`,
`/usr/local/share/terraform/plugins`, and `/usr/share/terraform/plugins`.
If a `terraform.d/plugins` directory exists in the current working directory If a `terraform.d/plugins` directory exists in the current working directory
then OpenTofu will also include that directory, regardless of your operating 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. 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 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 directory where the OpenTofu binary is installed. However, we do not recommend
using any provisioners except the built-in `file`, `local-exec`, and using any provisioners except the built-in `file`, `local-exec`, and
`remote-exec` provisioners. `remote-exec` provisioners.