From 32ef58435dc0f726098c8729843304955f014243 Mon Sep 17 00:00:00 2001 From: Ian Howell Date: Wed, 5 Feb 2020 17:21:18 -0600 Subject: [PATCH] Refactor management of config file paths This change replaces the usage of the clientcmd's PathOptions with a simpler filepath approach. The handling of the kube config file associated with airshipctl is now entirely managed by airshipctl. This also introduces the environment variable AIRSHIP_KUBECONFIG, which can be used to override the default location for airship's kube config. It can in turn be overridden by the command line argument. Prior to this change, the kube config object was created by creating a kubernetes client, then stripping off that client's config object. As a side effect, this change removes the middleman, and creates the kube config object directly from file. Change-Id: I99ba88d50a0f45c40597a58fe4b3fdfeb7d1467d --- pkg/config/config.go | 133 ++++++++++++++++--------------- pkg/config/config_test.go | 15 ++-- pkg/config/constants.go | 19 +++-- pkg/config/test_utils.go | 5 +- pkg/config/types.go | 11 ++- pkg/environment/settings.go | 92 ++++++++++----------- pkg/environment/settings_test.go | 119 ++++++++++++++++++++++----- 7 files changed, 237 insertions(+), 157 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 6396647f4..473c8157f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,21 +29,20 @@ import ( "sigs.k8s.io/yaml" "k8s.io/client-go/tools/clientcmd" - - kubeconfig "k8s.io/client-go/tools/clientcmd/api" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" "opendev.org/airship/airshipctl/pkg/util" ) -// Called from root to Load the initial configuration -func (c *Config) LoadConfig(configFileArg string, kPathOptions *clientcmd.PathOptions) error { - err := c.loadFromAirConfig(configFileArg) +// LoadConfig populates the Config object using the files found at +// airshipConfigPath and kubeConfigPath +func (c *Config) LoadConfig(airshipConfigPath, kubeConfigPath string) error { + err := c.loadFromAirConfig(airshipConfigPath) if err != nil { return err } - // Load or initialize the kubeconfig object from a file - err = c.loadKubeConfig(kPathOptions) + err = c.loadKubeConfig(kubeConfigPath) if err != nil { return err } @@ -52,36 +51,45 @@ func (c *Config) LoadConfig(configFileArg string, kPathOptions *clientcmd.PathOp return c.reconcileConfig() } -func (c *Config) loadFromAirConfig(configFileArg string) error { - // If it exists, Read the ConfigFile data - // Only care about the errors here, because there is a file - // And essentially I cannot use its data. - // airshipctl probable should stop - if configFileArg == "" { +// loadFromAirConfig populates the Config from the file found at airshipConfigPath. +// If there is no file at airshipConfigPath, this function does nothing. +// An error is returned if: +// * airshipConfigPath is the empty string +// * the file at airshipConfigPath is inaccessible +// * the file at airshipConfigPath cannot be marshaled into Config +func (c *Config) loadFromAirConfig(airshipConfigPath string) error { + if airshipConfigPath == "" { return errors.New("Configuration file location was not provided.") } + // Remember where I loaded the Config from - c.loadedConfigPath = configFileArg - // If I have a file to read, load from it + c.loadedConfigPath = airshipConfigPath - if _, err := os.Stat(configFileArg); os.IsNotExist(err) { + // If I can read from the file, load from it + if _, err := os.Stat(airshipConfigPath); os.IsNotExist(err) { return nil - } - return util.ReadYAMLFile(configFileArg, c) -} - -func (c *Config) loadKubeConfig(kPathOptions *clientcmd.PathOptions) error { - // Will need this for Persisting the changes - c.loadedPathOptions = kPathOptions - // Now at this point what I load might not reflect the associated kubeconfig yet - kConfig, err := kPathOptions.GetStartingConfig() - if err != nil { + } else if err != nil { return err } - // Store the kubeconfig object into an airship managed kubeconfig object - c.kubeConfig = kConfig - return nil + return util.ReadYAMLFile(airshipConfigPath, c) +} + +func (c *Config) loadKubeConfig(kubeConfigPath string) error { + // Will need this for persisting the changes + c.kubeConfigPath = kubeConfigPath + + // If I can read from the file, load from it + var err error + if _, err = os.Stat(kubeConfigPath); os.IsNotExist(err) { + c.kubeConfig = clientcmdapi.NewConfig() + return nil + } else if err != nil { + return err + } + + c.kubeConfig, err = clientcmd.LoadFromFile(kubeConfigPath) + return err } // reconcileConfig serves two functions: @@ -198,6 +206,7 @@ func (c *Config) rmConfigClusterStragglers(persistIt bool) bool { } return rccs } + func (c *Config) reconcileContexts(updatedClusterNames map[string]string) { for key, context := range c.kubeConfig.Contexts { // Check if the Cluster name referred to by the context @@ -254,7 +263,7 @@ func (c *Config) reconcileAuthInfos() { func (c *Config) reconcileCurrentContext() { // If the Airship current context is different that the current context in the kubeconfig // then - // - if the airship current context is valid, then updated kubeconfiug CC + // - if the airship current context is valid, then updated kubeconfig CC // - if the airship currentcontext is invalid, and the kubeconfig CC is valid, then create the reference // - otherwise , they are both empty. Make sure @@ -326,20 +335,17 @@ func (c *Config) EnsureComplete() error { return nil } -// This function is called to update the configuration in the file defined by the -// ConfigFile name -// It will completely overwrite the existing file, -// If the file specified by ConfigFile exists ts updates with the contents of the Config object -// If the file specified by ConfigFile does not exist it will create a new file. +// PersistConfig updates the airshipctl config and kubeconfig files to match +// the current Config and KubeConfig objects. +// If either file did not previously exist, the file will be created. +// Otherwise, the file will be overwritten func (c *Config) PersistConfig() error { - // Dont care if the file exists or not, will create if needed - // We are 100% overwriting the existing file - configyaml, err := c.ToYaml() + airshipConfigYaml, err := c.ToYaml() if err != nil { return err } - // WriteFile doesn't create the directory , create it if needed + // WriteFile doesn't create the directory, create it if needed configDir := filepath.Dir(c.loadedConfigPath) err = os.MkdirAll(configDir, 0755) if err != nil { @@ -347,19 +353,13 @@ func (c *Config) PersistConfig() error { } // Write the Airship Config file - err = ioutil.WriteFile(c.loadedConfigPath, configyaml, 0644) + err = ioutil.WriteFile(c.loadedConfigPath, airshipConfigYaml, 0644) if err != nil { return err } - // FIXME(howell): if this fails, then the results from the previous - // actions will persist, meaning that we might have overwritten our - // airshipconfig without updating our kubeconfig. A possible solution - // is to generate brand new config files and then write them at the - // end. That could still fail, but should be more robust - // Persist the kubeconfig file referenced - if err := clientcmd.ModifyConfig(c.loadedPathOptions, *c.kubeConfig, true); err != nil { + if err := clientcmd.WriteToFile(*c.kubeConfig, c.kubeConfigPath); err != nil { return err } @@ -386,14 +386,15 @@ func (c *Config) SetLoadedConfigPath(lcp string) { c.loadedConfigPath = lcp } -func (c *Config) LoadedPathOptions() *clientcmd.PathOptions { - return c.loadedPathOptions -} -func (c *Config) SetLoadedPathOptions(po *clientcmd.PathOptions) { - c.loadedPathOptions = po +func (c *Config) KubeConfigPath() string { + return c.kubeConfigPath } -func (c *Config) KubeConfig() *kubeconfig.Config { +func (c *Config) SetKubeConfigPath(kubeConfigPath string) { + c.kubeConfigPath = kubeConfigPath +} + +func (c *Config) KubeConfig() *clientcmdapi.Config { return c.kubeConfig } @@ -441,7 +442,7 @@ func (c *Config) AddCluster(theCluster *ClusterOptions) (*Cluster, error) { nCluster := NewCluster() c.Clusters[theCluster.Name].ClusterTypes[theCluster.ClusterType] = nCluster // Create a new Kubeconfig Cluster object as well - kcluster := kubeconfig.NewCluster() + kcluster := clientcmdapi.NewCluster() clusterName := NewClusterComplexName() clusterName.WithType(theCluster.Name, theCluster.ClusterType) nCluster.NameInKubeconf = clusterName.Name() @@ -541,7 +542,7 @@ func (c *Config) AddContext(theContext *ContextOptions) *Context { nContext := NewContext() c.Contexts[theContext.Name] = nContext // Create a new Kubeconfig Context object as well - kContext := kubeconfig.NewContext() + kContext := clientcmdapi.NewContext() nContext.NameInKubeconf = theContext.Name contextName := NewClusterComplexName() contextName.WithType(theContext.Name, theContext.ClusterType) @@ -645,7 +646,7 @@ func (c *Config) AddAuthInfo(theAuthInfo *AuthInfoOptions) *AuthInfo { nAuthInfo := NewAuthInfo() c.AuthInfos[theAuthInfo.Name] = nAuthInfo // Create a new Kubeconfig AuthInfo object as well - kAuthInfo := kubeconfig.NewAuthInfo() + kAuthInfo := clientcmdapi.NewAuthInfo() nAuthInfo.SetKubeAuthInfo(kAuthInfo) c.KubeConfig().AuthInfos[theAuthInfo.Name] = kAuthInfo @@ -739,10 +740,10 @@ func (c *Cluster) PrettyString() string { clusterName.ClusterName(), clusterName.ClusterType(), c) } -func (c *Cluster) KubeCluster() *kubeconfig.Cluster { +func (c *Cluster) KubeCluster() *clientcmdapi.Cluster { return c.kCluster } -func (c *Cluster) SetKubeCluster(kc *kubeconfig.Cluster) { +func (c *Cluster) SetKubeCluster(kc *clientcmdapi.Cluster) { c.kCluster = kc } @@ -777,11 +778,11 @@ func (c *Context) PrettyString() string { clusterName.ClusterName(), c.String()) } -func (c *Context) KubeContext() *kubeconfig.Context { +func (c *Context) KubeContext() *clientcmdapi.Context { return c.kContext } -func (c *Context) SetKubeContext(kc *kubeconfig.Context) { +func (c *Context) SetKubeContext(kc *clientcmdapi.Context) { c.kContext = kc } @@ -808,10 +809,10 @@ func (c *AuthInfo) String() string { return string(kyaml) } -func (c *AuthInfo) KubeAuthInfo() *kubeconfig.AuthInfo { +func (c *AuthInfo) KubeAuthInfo() *clientcmdapi.AuthInfo { return c.kAuthInfo } -func (c *AuthInfo) SetKubeAuthInfo(kc *kubeconfig.AuthInfo) { +func (c *AuthInfo) SetKubeAuthInfo(kc *clientcmdapi.AuthInfo) { c.kAuthInfo = kc } @@ -979,7 +980,7 @@ PLACEHOLDER UNTIL I IDENTIFY if CLIENTADM HAS SOMETHING LIKE THIS */ -func KClusterString(kCluster *kubeconfig.Cluster) string { +func KClusterString(kCluster *clientcmdapi.Cluster) string { yamlData, err := yaml.Marshal(&kCluster) if err != nil { return "" @@ -987,7 +988,8 @@ func KClusterString(kCluster *kubeconfig.Cluster) string { return string(yamlData) } -func KContextString(kContext *kubeconfig.Context) string { + +func KContextString(kContext *clientcmdapi.Context) string { yamlData, err := yaml.Marshal(&kContext) if err != nil { return "" @@ -995,7 +997,8 @@ func KContextString(kContext *kubeconfig.Context) string { return string(yamlData) } -func KAuthInfoString(kAuthInfo *kubeconfig.AuthInfo) string { + +func KAuthInfoString(kAuthInfo *clientcmdapi.AuthInfo) string { yamlData, err := yaml.Marshal(&kAuthInfo) if err != nil { return "" diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index cc0c5dd4d..0bb92ea4b 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -26,7 +26,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "k8s.io/client-go/tools/clientcmd" kubeconfig "k8s.io/client-go/tools/clientcmd/api" "opendev.org/airship/airshipctl/testutil" @@ -224,10 +223,11 @@ func TestPersistConfig(t *testing.T) { config := InitConfig(t) err := config.PersistConfig() - assert.NoErrorf(t, err, "Unable to persist configuration expected at %v", config.LoadedConfigPath()) + require.NoError(t, err) - kpo := config.LoadedPathOptions() - assert.NotNil(t, kpo) + // Check that the files were created + assert.FileExists(t, config.LoadedConfigPath()) + assert.FileExists(t, config.KubeConfigPath()) } func TestEnsureComplete(t *testing.T) { @@ -347,11 +347,10 @@ func TestPurge(t *testing.T) { require.NoError(t, err) airConfigFile := filepath.Join(tempDir, AirshipConfig) - kConfigFile := filepath.Join(tempDir, AirshipKubeConfig) config.SetLoadedConfigPath(airConfigFile) - kubePathOptions := clientcmd.NewDefaultPathOptions() - kubePathOptions.GlobalFile = kConfigFile - config.SetLoadedPathOptions(kubePathOptions) + + kConfigFile := filepath.Join(tempDir, AirshipKubeConfig) + config.kubeConfigPath = kConfigFile // Store it err = config.PersistConfig() diff --git a/pkg/config/constants.go b/pkg/config/constants.go index d06c73e8f..936c238d1 100644 --- a/pkg/config/constants.go +++ b/pkg/config/constants.go @@ -11,19 +11,22 @@ const ( AirshipClusterDefaultType = Target ) -//Sorted +// Sorted var AllClusterTypes = [2]string{Ephemeral, Target} // Constants defining default values const ( - AirshipConfigEnv = "airshipconf" - AirshipConfig = "config" - AirshipConfigDir = ".airship" - AirshipConfigKind = "Config" - AirshipConfigVersion = "v1alpha1" AirshipConfigGroup = "airshipit.org" + AirshipConfigVersion = "v1alpha1" AirshipConfigApiVersion = AirshipConfigGroup + "/" + AirshipConfigVersion - AirshipKubeConfig = "kubeconfig" + AirshipConfigKind = "Config" + + AirshipConfigDir = ".airship" + AirshipConfig = "config" + AirshipKubeConfig = "kubeconfig" + + AirshipConfigEnv = "AIRSHIPCONFIG" + AirshipKubeConfigEnv = "AIRSHIP_KUBECONFIG" ) // Constants defining CLI flags @@ -37,7 +40,7 @@ const ( FlagClusterType = "cluster-type" FlagContext = "context" FlagCurrentContext = "current-context" - FlagConfigFilePath = AirshipConfigEnv + FlagConfigFilePath = "airshipconf" FlagEmbedCerts = "embed-certs" FlagImpersonate = "as" FlagImpersonateGroup = "as-group" diff --git a/pkg/config/test_utils.go b/pkg/config/test_utils.go index a285d94e8..799e5b5f5 100644 --- a/pkg/config/test_utils.go +++ b/pkg/config/test_utils.go @@ -22,7 +22,6 @@ import ( "path/filepath" "testing" - "k8s.io/client-go/tools/clientcmd" kubeconfig "k8s.io/client-go/tools/clientcmd/api" "github.com/stretchr/testify/require" @@ -145,9 +144,7 @@ func InitConfig(t *testing.T) *Config { conf := NewConfig() - kubePathOptions := clientcmd.NewDefaultPathOptions() - kubePathOptions.GlobalFile = kubeConfigPath - err = conf.LoadConfig(configPath, kubePathOptions) + err = conf.LoadConfig(configPath, kubeConfigPath) require.NoError(t, err) return conf diff --git a/pkg/config/types.go b/pkg/config/types.go index 64d5a43d4..6750fd77e 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -19,7 +19,6 @@ package config import ( "net/url" - "k8s.io/client-go/tools/clientcmd" kubeconfig "k8s.io/client-go/tools/clientcmd/api" ) @@ -57,15 +56,15 @@ type Config struct { // Such as Bootstrap, Workflows, Document, etc ModulesConfig *Modules `json:"modules-config"` - // Private LoadedConfigPath is the full path to the the location of the config file - // from which these config was loaded + // loadedConfigPath is the full path to the the location of the config + // file from which this config was loaded // +not persisted in file loadedConfigPath string - // Private loadedPathOptions is the full path to the the location of the kubeconfig file - // associated with this airship config instance + // kubeConfigPath is the full path to the the location of the + // kubeconfig file associated with this airship config instance // +not persisted in file - loadedPathOptions *clientcmd.PathOptions + kubeConfigPath string // Private instance of Kube Config content as an object kubeConfig *kubeconfig.Config diff --git a/pkg/environment/settings.go b/pkg/environment/settings.go index e2f05be6c..8d1f14fba 100644 --- a/pkg/environment/settings.go +++ b/pkg/environment/settings.go @@ -3,7 +3,6 @@ package environment import ( "os" "path/filepath" - "strings" "github.com/spf13/cobra" @@ -64,67 +63,64 @@ func (a *AirshipCTLSettings) SetKubeConfigPath(kcp string) { func (a *AirshipCTLSettings) InitConfig() { a.SetConfig(config.NewConfig()) - a.setAirshipConfigPath() + a.initAirshipConfigPath() + a.initKubeConfigPath() - //Pass the airshipConfigPath and kubeConfig object - err := a.Config().LoadConfig(a.AirshipConfigPath(), a.setKubePathOptions()) + err := a.Config().LoadConfig(a.AirshipConfigPath(), a.KubeConfigPath()) if err != nil { // Should stop airshipctl log.Fatal(err) } } -func (a *AirshipCTLSettings) setAirshipConfigPath() { - // (1) If the airshipConfigPath was received as an argument its already set - if a.airshipConfigPath == "" { - // (2) If not , we can check if we got the Path via ENVIRONMENT variable, - // set the appropriate fields - a.setAirshipConfigPathFromEnv() +func (a *AirshipCTLSettings) initAirshipConfigPath() { + // The airshipConfigPath may already have been received as a command line argument + if a.airshipConfigPath != "" { + return } - // (3) Check if the a.airshipConfigPath is empty still at this point , use the defaults - acp, home := a.replaceHomePlaceholder(a.airshipConfigPath) - a.airshipConfigPath = acp - if a.airshipConfigPath == "" { - a.airshipConfigPath = filepath.Join(home, config.AirshipConfigDir, config.AirshipConfig) - } -} -// setAirshipConfigPathFromEnv Get AIRSHIP CONFIG from an environment variable if set -func (a *AirshipCTLSettings) setAirshipConfigPathFromEnv() { - // Check if AIRSHIPCONF env variable was set - // I have the path and name for the airship config file + // Otherwise, we can check if we got the path via ENVIRONMENT variable a.airshipConfigPath = os.Getenv(config.AirshipConfigEnv) -} - -func (a *AirshipCTLSettings) setKubePathOptions() *clientcmd.PathOptions { - // USe default expectations for Kubeconfig - kubePathOptions := clientcmd.NewDefaultPathOptions() - // No need to check the Environment , since we are relying on the kubeconfig defaults - // If we did not get an explicit kubeconfig definition on airshipctl - // as far as airshipctl is concerned will use the default expectations for the kubeconfig - // file location . This avoids messing up someones kubeconfig if they didnt explicitly want - // airshipctl to use it. - kcp, home := a.replaceHomePlaceholder(a.kubeConfigPath) - a.kubeConfigPath = kcp - if a.kubeConfigPath == "" { - a.kubeConfigPath = filepath.Join(home, config.AirshipConfigDir, config.AirshipKubeConfig) + if a.airshipConfigPath != "" { + return } - // We will always rely on tha airshipctl cli args or default for where to find kubeconfig - kubePathOptions.GlobalFile = a.kubeConfigPath - kubePathOptions.GlobalFileSubpath = a.kubeConfigPath - return kubePathOptions + // Otherwise, we'll try putting it in the home directory + homeDir := userHomeDir() + a.airshipConfigPath = filepath.Join(homeDir, config.AirshipConfigDir, config.AirshipConfig) } -func (a *AirshipCTLSettings) replaceHomePlaceholder(configPath string) (string, string) { - home, err := os.UserHomeDir() +func (a *AirshipCTLSettings) initKubeConfigPath() { + // NOTE(howell): This function will set the kubeConfigPath to the + // default location under the airship directory unless the user + // *explicitly* specifies a different location, either by setting the + // ENVIRONMENT variable or by passing a command line argument. + // This avoids messing up the user's kubeconfig if they didn't + // explicitly want airshipctl to use it. + + // The kubeConfigPath may already have been received as a command line argument + if a.kubeConfigPath != "" { + return + } + + // Otherwise, we can check if we got the path via ENVIRONMENT variable + a.kubeConfigPath = os.Getenv(config.AirshipKubeConfigEnv) + if a.kubeConfigPath != "" { + return + } + + // Otherwise, we'll try putting it in the home directory + homeDir := userHomeDir() + a.kubeConfigPath = filepath.Join(homeDir, config.AirshipConfigDir, config.AirshipKubeConfig) +} + +// userHomeDir is a utility function that wraps os.UserHomeDir and returns no +// errors. If the user has no home directory, the returned value will be the +// empty string +func userHomeDir() string { + homeDir, err := os.UserHomeDir() if err != nil { - // Use defaults under current directory - home = "" + homeDir = "" } - if configPath == "" { - return configPath, home - } - - return strings.Replace(configPath, HomePlaceholder, home, 1), home + return homeDir } diff --git a/pkg/environment/settings_test.go b/pkg/environment/settings_test.go index 4e31bf9f8..cb6a362d7 100644 --- a/pkg/environment/settings_test.go +++ b/pkg/environment/settings_test.go @@ -17,6 +17,7 @@ limitations under the License. package environment import ( + "io/ioutil" "os" "path/filepath" "testing" @@ -36,25 +37,107 @@ func TestInitFlags(t *testing.T) { assert.True(t, testCmd.HasPersistentFlags()) } -func TestSpecifyAirConfigFromEnv(t *testing.T) { - fakeConfig := "FakeConfigPath" - err := os.Setenv(config.AirshipConfigEnv, fakeConfig) +func TestInitConfig(t *testing.T) { + t.Run("DefaultToHomeDirectory", func(subTest *testing.T) { + // Set up a fake $HOME directory + testDir := makeTestDir(t) + defer deleteTestDir(t, testDir) + defer setHome(testDir)() + + var testSettings AirshipCTLSettings + expectedAirshipConfig := filepath.Join(testDir, config.AirshipConfigDir, config.AirshipConfig) + expectedKubeConfig := filepath.Join(testDir, config.AirshipConfigDir, config.AirshipKubeConfig) + + testSettings.InitConfig() + assert.Equal(t, expectedAirshipConfig, testSettings.AirshipConfigPath()) + assert.Equal(t, expectedKubeConfig, testSettings.KubeConfigPath()) + }) + + t.Run("PreferEnvToDefault", func(subTest *testing.T) { + // Set up a fake $HOME directory + testDir := makeTestDir(t) + defer deleteTestDir(t, testDir) + defer setHome(testDir)() + + var testSettings AirshipCTLSettings + expectedAirshipConfig := filepath.Join(testDir, "airshipEnv") + expectedKubeConfig := filepath.Join(testDir, "kubeEnv") + + os.Setenv(config.AirshipConfigEnv, expectedAirshipConfig) + os.Setenv(config.AirshipKubeConfigEnv, expectedKubeConfig) + defer os.Unsetenv(config.AirshipConfigEnv) + defer os.Unsetenv(config.AirshipKubeConfigEnv) + + testSettings.InitConfig() + assert.Equal(t, expectedAirshipConfig, testSettings.AirshipConfigPath()) + assert.Equal(t, expectedKubeConfig, testSettings.KubeConfigPath()) + }) + + t.Run("PreferCmdLineArgToDefault", func(subTest *testing.T) { + // Set up a fake $HOME directory + testDir := makeTestDir(t) + defer deleteTestDir(t, testDir) + defer setHome(testDir)() + + var testSettings AirshipCTLSettings + expectedAirshipConfig := filepath.Join(testDir, "airshipCmdLine") + expectedKubeConfig := filepath.Join(testDir, "kubeCmdLine") + + testSettings.SetAirshipConfigPath(expectedAirshipConfig) + testSettings.SetKubeConfigPath(expectedKubeConfig) + + // InitConfig should not change any values + testSettings.InitConfig() + assert.Equal(t, expectedAirshipConfig, testSettings.AirshipConfigPath()) + assert.Equal(t, expectedKubeConfig, testSettings.KubeConfigPath()) + }) + + t.Run("PreferCmdLineArgToEnv", func(subTest *testing.T) { + // Set up a fake $HOME directory + testDir := makeTestDir(t) + defer deleteTestDir(t, testDir) + defer setHome(testDir)() + + var testSettings AirshipCTLSettings + expectedAirshipConfig := filepath.Join(testDir, "airshipCmdLine") + expectedKubeConfig := filepath.Join(testDir, "kubeCmdLine") + + // set up "decoy" environment variables. These should be + // ignored, since we're simulating passing in command line + // arguments + wrongAirshipConfig := filepath.Join(testDir, "wrongAirshipConfig") + wrongKubeConfig := filepath.Join(testDir, "wrongKubeConfig") + os.Setenv(config.AirshipConfigEnv, wrongAirshipConfig) + os.Setenv(config.AirshipKubeConfigEnv, wrongKubeConfig) + defer os.Unsetenv(config.AirshipConfigEnv) + defer os.Unsetenv(config.AirshipKubeConfigEnv) + + testSettings.SetAirshipConfigPath(expectedAirshipConfig) + testSettings.SetKubeConfigPath(expectedKubeConfig) + + testSettings.InitConfig() + assert.Equal(t, expectedAirshipConfig, testSettings.AirshipConfigPath()) + assert.Equal(t, expectedKubeConfig, testSettings.KubeConfigPath()) + }) +} + +func makeTestDir(t *testing.T) string { + testDir, err := ioutil.TempDir("", "airship-test") require.NoError(t, err) - - settings := &AirshipCTLSettings{} - settings.InitConfig() - - assert.EqualValues(t, fakeConfig, settings.AirshipConfigPath()) + return testDir } -func TestGetSetPaths(t *testing.T) { - settings := &AirshipCTLSettings{} - settings.InitConfig() - airConfigFile := filepath.Join(config.AirshipConfigDir, config.AirshipConfig) - kConfigFile := filepath.Join(config.AirshipConfigDir, config.AirshipKubeConfig) - settings.SetAirshipConfigPath(airConfigFile) - assert.EqualValues(t, airConfigFile, settings.AirshipConfigPath()) - - settings.SetKubeConfigPath(kConfigFile) - assert.EqualValues(t, kConfigFile, settings.KubeConfigPath()) +func deleteTestDir(t *testing.T, path string) { + err := os.Remove(path) + require.NoError(t, err) +} + +// setHome sets the HOME environment variable to `path`, and returns a function +// that can be used to reset HOME to its original value +func setHome(path string) (resetHome func()) { + oldHome := os.Getenv("HOME") + os.Setenv("HOME", path) + return func() { + os.Setenv("HOME", oldHome) + } }