cliconfig: Allow breaking the dependency lock file using the environment

Since it's already possible to activate the dependency lock file using an
environment variable, we should allow opting in to it having broken
behavior using the environment too.

It's kinda odd in retrospect that TF_PLUGIN_CACHE_DIR is the only setting
we allow to be configured both in the environment and the CLI
configuration. That means that the infrastructure for dealing with that
situation was relatively immature here and so I did some light refactoring
to make it unit-testable without actually modifying the test program's
environment.
This commit is contained in:
Martin Atkins 2023-02-21 13:51:57 -08:00
parent 3d1a58d5b5
commit a86cef4d50
2 changed files with 177 additions and 2 deletions

View File

@ -14,6 +14,7 @@ import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/hashicorp/hcl" "github.com/hashicorp/hcl"
@ -22,6 +23,7 @@ import (
) )
const pluginCacheDirEnvVar = "TF_PLUGIN_CACHE_DIR" const pluginCacheDirEnvVar = "TF_PLUGIN_CACHE_DIR"
const pluginCacheMayBreakLockFileEnvVar = "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE"
// Config is the structure of the configuration for the Terraform CLI. // Config is the structure of the configuration for the Terraform CLI.
// //
@ -220,9 +222,14 @@ func loadConfigDir(path string) (*Config, tfdiags.Diagnostics) {
// Any values specified in this config should override those set in the // Any values specified in this config should override those set in the
// configuration file. // configuration file.
func EnvConfig() *Config { func EnvConfig() *Config {
env := makeEnvMap(os.Environ())
return envConfig(env)
}
func envConfig(env map[string]string) *Config {
config := &Config{} config := &Config{}
if envPluginCacheDir := os.Getenv(pluginCacheDirEnvVar); envPluginCacheDir != "" { if envPluginCacheDir := env[pluginCacheDirEnvVar]; envPluginCacheDir != "" {
// No Expandenv here, because expanding environment variables inside // No Expandenv here, because expanding environment variables inside
// an environment variable would be strange and seems unnecessary. // an environment variable would be strange and seems unnecessary.
// (User can expand variables into the value while setting it using // (User can expand variables into the value while setting it using
@ -230,9 +237,34 @@ func EnvConfig() *Config {
config.PluginCacheDir = envPluginCacheDir config.PluginCacheDir = envPluginCacheDir
} }
if envMayBreak := env[pluginCacheMayBreakLockFileEnvVar]; envMayBreak != "" && envMayBreak != "0" {
// This is an environment variable analog to the
// plugin_cache_may_break_dependency_lock_file setting. If either this
// or the config file setting are enabled then it's enabled; there is
// no way to override back to false if either location sets this to
// true.
config.PluginCacheMayBreakDependencyLockFile = true
}
return config return config
} }
func makeEnvMap(environ []string) map[string]string {
if len(environ) == 0 {
return nil
}
ret := make(map[string]string, len(environ))
for _, entry := range environ {
eq := strings.IndexByte(entry, '=')
if eq == -1 {
continue
}
ret[entry[:eq]] = entry[eq+1:]
}
return ret
}
// Validate checks for errors in the configuration that cannot be detected // Validate checks for errors in the configuration that cannot be detected
// just by HCL decoding, returning any problems as diagnostics. // just by HCL decoding, returning any problems as diagnostics.
// //
@ -328,6 +360,12 @@ func (c *Config) Merge(c2 *Config) *Config {
result.PluginCacheDir = c2.PluginCacheDir result.PluginCacheDir = c2.PluginCacheDir
} }
if c.PluginCacheMayBreakDependencyLockFile || c2.PluginCacheMayBreakDependencyLockFile {
// This setting saturates to "on"; once either configuration sets it,
// there is no way to override it back to off again.
result.PluginCacheMayBreakDependencyLockFile = true
}
if (len(c.Hosts) + len(c2.Hosts)) > 0 { if (len(c.Hosts) + len(c2.Hosts)) > 0 {
result.Hosts = make(map[string]*ConfigHost) result.Hosts = make(map[string]*ConfigHost)
for name, host := range c.Hosts { for name, host := range c.Hosts {

View File

@ -31,7 +31,7 @@ func TestLoadConfig(t *testing.T) {
} }
} }
func TestLoadConfig_env(t *testing.T) { func TestLoadConfig_envSubst(t *testing.T) {
defer os.Unsetenv("TFTEST") defer os.Unsetenv("TFTEST")
os.Setenv("TFTEST", "hello") os.Setenv("TFTEST", "hello")
@ -55,6 +55,141 @@ func TestLoadConfig_env(t *testing.T) {
} }
} }
func TestEnvConfig(t *testing.T) {
tests := map[string]struct {
env map[string]string
want *Config
}{
"no environment variables": {
nil,
&Config{},
},
"TF_PLUGIN_CACHE_DIR=boop": {
map[string]string{
"TF_PLUGIN_CACHE_DIR": "boop",
},
&Config{
PluginCacheDir: "boop",
},
},
"TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE=anything_except_zero": {
map[string]string{
"TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": "anything_except_zero",
},
&Config{
PluginCacheMayBreakDependencyLockFile: true,
},
},
"TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE=0": {
map[string]string{
"TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": "0",
},
&Config{},
},
"TF_PLUGIN_CACHE_DIR and TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": {
map[string]string{
"TF_PLUGIN_CACHE_DIR": "beep",
"TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": "1",
},
&Config{
PluginCacheDir: "beep",
PluginCacheMayBreakDependencyLockFile: true,
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := envConfig(test.env)
want := test.want
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
})
}
}
func TestMakeEnvMap(t *testing.T) {
tests := map[string]struct {
environ []string
want map[string]string
}{
"nil": {
nil,
nil,
},
"one": {
[]string{
"FOO=bar",
},
map[string]string{
"FOO": "bar",
},
},
"many": {
[]string{
"FOO=1",
"BAR=2",
"BAZ=3",
},
map[string]string{
"FOO": "1",
"BAR": "2",
"BAZ": "3",
},
},
"conflict": {
[]string{
"FOO=1",
"BAR=1",
"FOO=2",
},
map[string]string{
"BAR": "1",
"FOO": "2", // Last entry of each name wins
},
},
"empty_val": {
[]string{
"FOO=",
},
map[string]string{
"FOO": "",
},
},
"no_equals": {
[]string{
"FOO=bar",
"INVALID",
},
map[string]string{
"FOO": "bar",
},
},
"multi_equals": {
[]string{
"FOO=bar=baz=boop",
},
map[string]string{
"FOO": "bar=baz=boop",
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
got := makeEnvMap(test.environ)
want := test.want
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("wrong result\n%s", diff)
}
})
}
}
func TestLoadConfig_hosts(t *testing.T) { func TestLoadConfig_hosts(t *testing.T) {
got, diags := loadConfigFile(filepath.Join(fixtureDir, "hosts")) got, diags := loadConfigFile(filepath.Join(fixtureDir, "hosts"))
if len(diags) != 0 { if len(diags) != 0 {
@ -284,6 +419,7 @@ func TestConfig_Merge(t *testing.T) {
}, },
}, },
}, },
PluginCacheMayBreakDependencyLockFile: true,
} }
expected := &Config{ expected := &Config{
@ -338,6 +474,7 @@ func TestConfig_Merge(t *testing.T) {
}, },
}, },
}, },
PluginCacheMayBreakDependencyLockFile: true,
} }
actual := c1.Merge(c2) actual := c1.Merge(c2)