mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Support the XDG Base Directory Specification (#1200)
Signed-off-by: Mario Valderrama <mario.valderrama@ionos.com>
This commit is contained in:
parent
f96cb50405
commit
e84b2f7f95
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
151
internal/command/cliconfig/config_unix_test.go
Normal file
151
internal/command/cliconfig/config_unix_test.go
Normal 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)) })
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user