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 "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))
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
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
|
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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user