// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package cliconfig import ( "context" "os" "path/filepath" "reflect" "testing" "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" "github.com/opentofu/opentofu/internal/tfdiags" ) // This is the directory where our test fixtures are. const fixtureDir = "./testdata" func TestLoadConfig_ignore_providers_provisioners(t *testing.T) { // There used to be providers and provisioners in cli config files // We want to make sure config files load properly despite their // possible presence. c, err := loadConfigFile(filepath.Join(fixtureDir, "config")) if err != nil { t.Fatalf("err: %s", err) } expected := &Config{ Hosts: map[string]*ConfigHost{ "example.com": { Services: map[string]interface{}{ "modules.v1": "https://example.com/", }, }, }, } if !reflect.DeepEqual(c, expected) { t.Fatalf("bad: %#v", c) } } func TestLoadConfig_non_existing_file(t *testing.T) { tmpDir := os.TempDir() cliTmpFile := filepath.Join(tmpDir, "dev.tfrc") t.Setenv("TF_CLI_CONFIG_FILE", cliTmpFile) c, errs := LoadConfig(context.Background()) if errs.HasErrors() || c.Validate().HasErrors() { t.Fatalf("err: %s", errs) } hasStatFileWarn := false for _, err := range errs { if err.Severity() == tfdiags.Warning && err.Description().Summary == "Unable to open CLI configuration file" { hasStatFileWarn = true break } } if !hasStatFileWarn { t.Fatal("expecting a warning message because of nonexisting CLI configuration file") } } 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) { got, diags := loadConfigFile(filepath.Join(fixtureDir, "hosts")) if len(diags) != 0 { t.Fatalf("%s", diags.Err()) } want := &Config{ Hosts: map[string]*ConfigHost{ "example.com": { Services: map[string]interface{}{ "modules.v1": "https://example.com/", }, }, }, } if !reflect.DeepEqual(got, want) { t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want)) } } func TestLoadConfig_credentials(t *testing.T) { got, err := loadConfigFile(filepath.Join(fixtureDir, "credentials")) if err != nil { t.Fatal(err) } want := &Config{ Credentials: map[string]map[string]interface{}{ "example.com": map[string]interface{}{ "token": "foo the bar baz", }, "example.net": map[string]interface{}{ "username": "foo", "password": "baz", }, }, CredentialsHelpers: map[string]*ConfigCredentialsHelper{ "foo": &ConfigCredentialsHelper{ Args: []string{"bar", "baz"}, }, }, } if !reflect.DeepEqual(got, want) { t.Errorf("wrong result\ngot: %swant: %s", spew.Sdump(got), spew.Sdump(want)) } } func TestConfigValidate(t *testing.T) { tests := map[string]struct { Config *Config DiagCount int }{ "nil": { nil, 0, }, "empty": { &Config{}, 0, }, "host good": { &Config{ Hosts: map[string]*ConfigHost{ "example.com": {}, }, }, 0, }, "host with bad hostname": { &Config{ Hosts: map[string]*ConfigHost{ "example..com": {}, }, }, 1, // host block has invalid hostname }, "credentials good": { &Config{ Credentials: map[string]map[string]interface{}{ "example.com": map[string]interface{}{ "token": "foo", }, }, }, 0, }, "credentials with bad hostname": { &Config{ Credentials: map[string]map[string]interface{}{ "example..com": map[string]interface{}{ "token": "foo", }, }, }, 1, // credentials block has invalid hostname }, "credentials helper good": { &Config{ CredentialsHelpers: map[string]*ConfigCredentialsHelper{ "foo": {}, }, }, 0, }, "credentials helper too many": { &Config{ CredentialsHelpers: map[string]*ConfigCredentialsHelper{ "foo": {}, "bar": {}, }, }, 1, // no more than one credentials_helper block allowed }, "provider_installation good none": { &Config{ ProviderInstallation: nil, }, 0, }, "provider_installation good one": { &Config{ ProviderInstallation: []*ProviderInstallation{ {}, }, }, 0, }, "provider_installation too many": { &Config{ ProviderInstallation: []*ProviderInstallation{ {}, {}, }, }, 1, // no more than one provider_installation block allowed }, "plugin_cache_dir does not exist": { &Config{ PluginCacheDir: "fake", }, 1, // The specified plugin cache dir %s cannot be opened }, } for name, test := range tests { t.Run(name, func(t *testing.T) { diags := test.Config.Validate() if len(diags) != test.DiagCount { t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) for _, diag := range diags { t.Logf("- %#v", diag.Description()) } } }) } } func TestConfig_Merge(t *testing.T) { c1 := &Config{ Hosts: map[string]*ConfigHost{ "example.com": { Services: map[string]interface{}{ "modules.v1": "http://example.com/", }, }, }, Credentials: map[string]map[string]interface{}{ "foo": { "bar": "baz", }, }, CredentialsHelpers: map[string]*ConfigCredentialsHelper{ "buz": {}, }, ProviderInstallation: []*ProviderInstallation{ { Methods: []*ProviderInstallationMethod{ {Location: ProviderInstallationFilesystemMirror("a")}, {Location: ProviderInstallationFilesystemMirror("b")}, }, }, { Methods: []*ProviderInstallationMethod{ {Location: ProviderInstallationFilesystemMirror("c")}, }, }, }, OCIDefaultCredentials: []*OCIDefaultCredentials{ { DiscoverAmbientCredentials: false, }, }, OCIRepositoryCredentials: []*OCIRepositoryCredentials{ { RepositoryPrefix: "example.com", DockerCredentialHelper: "osxkeychain", }, }, } c2 := &Config{ Hosts: map[string]*ConfigHost{ "example.net": { Services: map[string]interface{}{ "modules.v1": "https://example.net/", }, }, }, Credentials: map[string]map[string]interface{}{ "fee": { "bur": "bez", }, }, CredentialsHelpers: map[string]*ConfigCredentialsHelper{ "biz": {}, }, ProviderInstallation: []*ProviderInstallation{ { Methods: []*ProviderInstallationMethod{ {Location: ProviderInstallationFilesystemMirror("d")}, }, }, }, PluginCacheMayBreakDependencyLockFile: true, OCIDefaultCredentials: []*OCIDefaultCredentials{ { DefaultDockerCredentialHelper: "osxkeychain", }, }, OCIRepositoryCredentials: []*OCIRepositoryCredentials{ { RepositoryPrefix: "example.net", DockerCredentialHelper: "osxkeychain", }, }, } expected := &Config{ Hosts: map[string]*ConfigHost{ "example.com": { Services: map[string]interface{}{ "modules.v1": "http://example.com/", }, }, "example.net": { Services: map[string]interface{}{ "modules.v1": "https://example.net/", }, }, }, Credentials: map[string]map[string]interface{}{ "foo": { "bar": "baz", }, "fee": { "bur": "bez", }, }, CredentialsHelpers: map[string]*ConfigCredentialsHelper{ "buz": {}, "biz": {}, }, ProviderInstallation: []*ProviderInstallation{ { Methods: []*ProviderInstallationMethod{ {Location: ProviderInstallationFilesystemMirror("a")}, {Location: ProviderInstallationFilesystemMirror("b")}, }, }, { Methods: []*ProviderInstallationMethod{ {Location: ProviderInstallationFilesystemMirror("c")}, }, }, { Methods: []*ProviderInstallationMethod{ {Location: ProviderInstallationFilesystemMirror("d")}, }, }, }, PluginCacheMayBreakDependencyLockFile: true, OCIDefaultCredentials: []*OCIDefaultCredentials{ { DiscoverAmbientCredentials: false, }, { DefaultDockerCredentialHelper: "osxkeychain", }, }, OCIRepositoryCredentials: []*OCIRepositoryCredentials{ { RepositoryPrefix: "example.com", DockerCredentialHelper: "osxkeychain", }, { RepositoryPrefix: "example.net", DockerCredentialHelper: "osxkeychain", }, }, } actual := c1.Merge(c2) if diff := cmp.Diff(expected, actual); diff != "" { t.Fatalf("wrong result\n%s", diff) } }