diff --git a/changes/20231010151706.feature b/changes/20231010151706.feature new file mode 100644 index 0000000000..4a3404809b --- /dev/null +++ b/changes/20231010151706.feature @@ -0,0 +1 @@ +:sparkles: [`environment`] Add `GetEnvironmentVariable()` to fetch a specific environment variable diff --git a/changes/20231010151811.feature b/changes/20231010151811.feature new file mode 100644 index 0000000000..2493c0a70a --- /dev/null +++ b/changes/20231010151811.feature @@ -0,0 +1 @@ +:sparkles: [`environment`] Extend `GetEnvironmentVariables()` to accept optional ...dotEnvFiles so that [.env](https://github.com/bkeepers/dotenv) files can be loaded into the current environment diff --git a/utils/environment/current.go b/utils/environment/current.go index ff47cf71d0..b422e315b9 100644 --- a/utils/environment/current.go +++ b/utils/environment/current.go @@ -1,11 +1,14 @@ package environment import ( + "fmt" "os" "os/user" + "github.com/joho/godotenv" "github.com/mitchellh/go-homedir" + "github.com/ARM-software/golang-utils/utils/commonerrors" "github.com/ARM-software/golang-utils/utils/filesystem" ) @@ -25,7 +28,13 @@ func (c *currentEnv) GetCurrentUser() (currentUser *user.User) { return } -func (c *currentEnv) GetEnvironmentVariables() (variables []IEnvironmentVariable) { +// GetEnvironmentVariables returns the current environment variable (and optionally those in the supplied in `dotEnvFiles`) +// `dotEnvFiles` corresponds to `.env` files present on the machine and follows the mechanism described by https://github.com/bkeepers/dotenv +func (c *currentEnv) GetEnvironmentVariables(dotEnvFiles ...string) (variables []IEnvironmentVariable) { + if len(dotEnvFiles) > 0 { // if no args, then it will attempt to load .env + _ = godotenv.Load(dotEnvFiles...) // ignore error (specifically on loading .env) consistent with config.LoadFromEnvironment + } + curentEnv := os.Environ() for i := range curentEnv { envvar, err := ParseEnvironmentVariable(curentEnv[i]) @@ -41,6 +50,17 @@ func (c *currentEnv) GetFilesystem() filesystem.FS { return filesystem.NewStandardFileSystem() } +// GetEnvironmentVariable searches the current environment (and optionally dotEnvFiles) for a specific environment variable `envvar`. +func (c *currentEnv) GetEnvironmentVariable(envvar string, dotEnvFiles ...string) (value IEnvironmentVariable, err error) { + envvars := c.GetEnvironmentVariables(dotEnvFiles...) + for i := range envvars { + if envvars[i].GetKey() == envvar { + return envvars[i], nil + } + } + return nil, fmt.Errorf("%w: environment variable '%v' not set", commonerrors.ErrNotFound, envvar) +} + // NewCurrentEnvironment returns system current environment. func NewCurrentEnvironment() IEnvironment { return ¤tEnv{} diff --git a/utils/environment/current_test.go b/utils/environment/current_test.go index 3d9525df91..b2af4b06c8 100644 --- a/utils/environment/current_test.go +++ b/utils/environment/current_test.go @@ -1,12 +1,21 @@ package environment import ( + "fmt" "os" "testing" "github.com/bxcodec/faker/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/ARM-software/golang-utils/utils/commonerrors" + "github.com/ARM-software/golang-utils/utils/commonerrors/errortest" + "github.com/ARM-software/golang-utils/utils/filesystem" +) + +const ( + dotEnvPattern = ".env.*" ) func TestNewCurrentEnvironment(t *testing.T) { @@ -20,11 +29,129 @@ func TestNewCurrentEnvironment(t *testing.T) { } func Test_currentEnv_GetEnvironmentVariables(t *testing.T) { - os.Clearenv() - require.NoError(t, os.Setenv("test1", faker.Sentence())) - require.NoError(t, os.Setenv("test2", faker.Sentence())) - current := NewCurrentEnvironment() - envVars := current.GetEnvironmentVariables() - assert.Len(t, envVars, 2) - assert.False(t, envVars[0].Equal(envVars[1])) + t.Run("No dotenv files", func(t *testing.T) { + os.Clearenv() + require.NoError(t, os.Setenv("test1", faker.Sentence())) + require.NoError(t, os.Setenv("test2", faker.Sentence())) + current := NewCurrentEnvironment() + envVars := current.GetEnvironmentVariables() + assert.Len(t, envVars, 2) + assert.False(t, envVars[0].Equal(envVars[1])) + }) + + t.Run("With dotenv files", func(t *testing.T) { + os.Clearenv() + require.NoError(t, os.Setenv("test1", faker.Sentence())) + require.NoError(t, os.Setenv("test2", faker.Sentence())) + + dotenv1, err := filesystem.TempFileInTempDir(dotEnvPattern) + require.NoError(t, err) + defer func() { _ = dotenv1.Close() }() + test3 := NewEnvironmentVariable("test3", faker.Sentence()) + _, err = dotenv1.WriteString(test3.String()) + require.NoError(t, err) + err = dotenv1.Close() + require.NoError(t, err) + + dotenv2, err := filesystem.TempFileInTempDir(dotEnvPattern) + require.NoError(t, err) + defer func() { _ = dotenv2.Close() }() + test4 := NewEnvironmentVariable("test4", faker.Sentence()) + _, err = dotenv2.WriteString(fmt.Sprintf("%v\n", test4.String())) + require.NoError(t, err) + test5 := NewEnvironmentVariable("test5", faker.Sentence()) + _, err = dotenv2.WriteString(fmt.Sprintf("%v\n", test5.String())) + require.NoError(t, err) + err = dotenv2.Close() + require.NoError(t, err) + + current := NewCurrentEnvironment() + envVars := current.GetEnvironmentVariables(dotenv1.Name(), dotenv2.Name()) + assert.Len(t, envVars, 5) + assert.False(t, envVars[0].Equal(envVars[1])) + assert.True(t, envVars[2].Equal(test3)) + assert.True(t, envVars[3].Equal(test4)) + assert.True(t, envVars[4].Equal(test5)) + }) +} + +func Test_currentenv_GetEnvironmentVariable(t *testing.T) { + t.Run("Env var exists", func(t *testing.T) { + os.Clearenv() + test := NewEnvironmentVariable(faker.Word(), faker.Sentence()) + require.NoError(t, os.Setenv(test.GetKey(), test.GetValue())) + + current := NewCurrentEnvironment() + + actual, err := current.GetEnvironmentVariable(test.GetKey()) + assert.NoError(t, err) + assert.Equal(t, test, actual) + }) + + t.Run("Env var not exists", func(t *testing.T) { + os.Clearenv() + test := NewEnvironmentVariable(faker.Word(), faker.Sentence()) + current := NewCurrentEnvironment() + + actual, err := current.GetEnvironmentVariable(faker.Word()) + errortest.AssertError(t, err, commonerrors.ErrNotFound) + assert.NotEqual(t, test, actual) + }) + + t.Run("With dotenv files", func(t *testing.T) { + os.Clearenv() + test1 := NewEnvironmentVariable("test1", faker.Sentence()) + test2 := NewEnvironmentVariable("test2", faker.Sentence()) + + require.NoError(t, os.Setenv(test1.GetKey(), test1.GetValue())) + require.NoError(t, os.Setenv(test2.GetKey(), test2.GetValue())) + + dotenv1, err := filesystem.TempFileInTempDir(dotEnvPattern) + require.NoError(t, err) + defer func() { _ = dotenv1.Close() }() + test3 := NewEnvironmentVariable("test3", faker.Sentence()) + _, err = dotenv1.WriteString(test3.String()) + require.NoError(t, err) + err = dotenv1.Close() + require.NoError(t, err) + + dotenv2, err := filesystem.TempFileInTempDir(dotEnvPattern) + require.NoError(t, err) + defer func() { _ = dotenv2.Close() }() + test4 := NewEnvironmentVariable("test4", faker.Sentence()) + _, err = dotenv2.WriteString(fmt.Sprintf("%v\n", test4.String())) + require.NoError(t, err) + test5 := NewEnvironmentVariable("test5", faker.Sentence()) + _, err = dotenv2.WriteString(fmt.Sprintf("%v\n", test5.String())) + require.NoError(t, err) + err = dotenv2.Close() + require.NoError(t, err) + + current := NewCurrentEnvironment() + test1Actual, err := current.GetEnvironmentVariable(test1.GetKey(), dotenv1.Name(), dotenv2.Name()) + assert.NoError(t, err) + assert.Equal(t, test1, test1Actual) + test2Actual, err := current.GetEnvironmentVariable(test2.GetKey(), dotenv1.Name(), dotenv2.Name()) + assert.NoError(t, err) + assert.Equal(t, test2, test2Actual) + test3Actual, err := current.GetEnvironmentVariable(test3.GetKey(), dotenv1.Name(), dotenv2.Name()) + assert.NoError(t, err) + assert.Equal(t, test3, test3Actual) + test4Actual, err := current.GetEnvironmentVariable(test4.GetKey(), dotenv1.Name(), dotenv2.Name()) + assert.NoError(t, err) + assert.Equal(t, test4, test4Actual) + test5Actual, err := current.GetEnvironmentVariable(test5.GetKey(), dotenv1.Name(), dotenv2.Name()) + assert.NoError(t, err) + assert.Equal(t, test5, test5Actual) + + os.Clearenv() + + test3Missing, err := current.GetEnvironmentVariable(test3.GetKey(), dotenv2.Name()) + errortest.AssertError(t, err, commonerrors.ErrNotFound) + assert.NotEqual(t, test3, test3Missing) + + testMissing, err := current.GetEnvironmentVariable(faker.Word(), dotenv1.Name(), dotenv2.Name()) + errortest.AssertError(t, err, commonerrors.ErrNotFound) + assert.Nil(t, testMissing) + }) } diff --git a/utils/environment/interfaces.go b/utils/environment/interfaces.go index b409ffcf82..df474ef77d 100644 --- a/utils/environment/interfaces.go +++ b/utils/environment/interfaces.go @@ -30,8 +30,12 @@ type IEnvironmentVariable interface { type IEnvironment interface { // GetCurrentUser returns the environment current user. GetCurrentUser() *user.User - // GetEnvironmentVariables returns the variables defining the environment. - GetEnvironmentVariables() []IEnvironmentVariable + // GetEnvironmentVariables returns the variables defining the environment (and optionally those supplied in `dotEnvFiles`) + // `dotEnvFiles` corresponds to `.env` files present on the machine and follows the mechanism described by https://github.com/bkeepers/dotenv + GetEnvironmentVariables(dotEnvFiles ...string) []IEnvironmentVariable // GetFilesystem returns the filesystem associated with the current environment GetFilesystem() filesystem.FS + // GetEnvironmentVariable returns the environment variable corresponding to `envvar` or an error if it not set. optionally it searches `dotEnvFiles` files too + // `dotEnvFiles` corresponds to `.env` files present on the machine and follows the mechanism described by https://github.com/bkeepers/dotenv + GetEnvironmentVariable(envvar string, dotEnvFiles ...string) (IEnvironmentVariable, error) } diff --git a/utils/mocks/mock_environment.go b/utils/mocks/mock_environment.go index af86905acd..1bf88700f5 100644 --- a/utils/mocks/mock_environment.go +++ b/utils/mocks/mock_environment.go @@ -173,18 +173,42 @@ func (mr *MockIEnvironmentMockRecorder) GetCurrentUser() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentUser", reflect.TypeOf((*MockIEnvironment)(nil).GetCurrentUser)) } +// GetEnvironmentVariable mocks base method. +func (m *MockIEnvironment) GetEnvironmentVariable(arg0 string, arg1 ...string) (environment.IEnvironmentVariable, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetEnvironmentVariable", varargs...) + ret0, _ := ret[0].(environment.IEnvironmentVariable) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEnvironmentVariable indicates an expected call of GetEnvironmentVariable. +func (mr *MockIEnvironmentMockRecorder) GetEnvironmentVariable(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvironmentVariable", reflect.TypeOf((*MockIEnvironment)(nil).GetEnvironmentVariable), varargs...) +} + // GetEnvironmentVariables mocks base method. -func (m *MockIEnvironment) GetEnvironmentVariables() []environment.IEnvironmentVariable { +func (m *MockIEnvironment) GetEnvironmentVariables(arg0 ...string) []environment.IEnvironmentVariable { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetEnvironmentVariables") + varargs := []interface{}{} + for _, a := range arg0 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetEnvironmentVariables", varargs...) ret0, _ := ret[0].([]environment.IEnvironmentVariable) return ret0 } // GetEnvironmentVariables indicates an expected call of GetEnvironmentVariables. -func (mr *MockIEnvironmentMockRecorder) GetEnvironmentVariables() *gomock.Call { +func (mr *MockIEnvironmentMockRecorder) GetEnvironmentVariables(arg0 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvironmentVariables", reflect.TypeOf((*MockIEnvironment)(nil).GetEnvironmentVariables)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEnvironmentVariables", reflect.TypeOf((*MockIEnvironment)(nil).GetEnvironmentVariables), arg0...) } // GetFilesystem mocks base method.