From 62828fe0dfddf587be573a8e928d3dfd73116e82 Mon Sep 17 00:00:00 2001 From: Josh Jennings Date: Tue, 10 Oct 2023 15:18:50 +0100 Subject: [PATCH 1/3] `environment` Add GetEnvironmentVariable() to try and fetch specific environment variable --- changes/20231010151706.feature | 1 + changes/20231010151811.feature | 1 + utils/environment/current.go | 21 ++++- utils/environment/current_test.go | 134 ++++++++++++++++++++++++++++-- utils/environment/interfaces.go | 4 +- utils/mocks/mock_environment.go | 35 ++++++-- 6 files changed, 181 insertions(+), 15 deletions(-) create mode 100644 changes/20231010151706.feature create mode 100644 changes/20231010151811.feature diff --git a/changes/20231010151706.feature b/changes/20231010151706.feature new file mode 100644 index 0000000000..4b37c100d7 --- /dev/null +++ b/changes/20231010151706.feature @@ -0,0 +1 @@ +`environment` Add GetEnvironmentVariable() to try and fetch specific environment variable diff --git a/changes/20231010151811.feature b/changes/20231010151811.feature new file mode 100644 index 0000000000..e703d22f0e --- /dev/null +++ b/changes/20231010151811.feature @@ -0,0 +1 @@ +`environment` Add optional ...dotEnvFiles to GetEnvironmentVariables() and GetEnvironmentVariable() so that .env files can be loaded into the current environment diff --git a/utils/environment/current.go b/utils/environment/current.go index ff47cf71d0..ae3ee38820 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,12 @@ 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 dotEnvFiles) +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 +49,17 @@ func (c *currentEnv) GetFilesystem() filesystem.FS { return filesystem.NewStandardFileSystem() } +// GetEnvironmentVariable searchs the current environment (and optionally dotEnvFiles) for a specific env var +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..211fcde23a 100644 --- a/utils/environment/current_test.go +++ b/utils/environment/current_test.go @@ -1,14 +1,22 @@ package environment import ( + "fmt" "os" "testing" + "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" "github.com/bxcodec/faker/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +const ( + dotEnvPattern = ".env.*" +) + func TestNewCurrentEnvironment(t *testing.T) { current := NewCurrentEnvironment() currentUser := current.GetCurrentUser() @@ -20,11 +28,123 @@ 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()) + dotenv1.WriteString(test3.String()) + 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()) + dotenv2.WriteString(fmt.Sprintf("%v\n", test4.String())) + test5 := NewEnvironmentVariable("test5", faker.Sentence()) + dotenv2.WriteString(fmt.Sprintf("%v\n", test5.String())) + 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()) + dotenv1.WriteString(test3.String()) + 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()) + dotenv2.WriteString(fmt.Sprintf("%v\n", test4.String())) + test5 := NewEnvironmentVariable("test5", faker.Sentence()) + dotenv2.WriteString(fmt.Sprintf("%v\n", test5.String())) + 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..015335bac9 100644 --- a/utils/environment/interfaces.go +++ b/utils/environment/interfaces.go @@ -31,7 +31,9 @@ type IEnvironment interface { // GetCurrentUser returns the environment current user. GetCurrentUser() *user.User // GetEnvironmentVariables returns the variables defining the environment. - GetEnvironmentVariables() []IEnvironmentVariable + GetEnvironmentVariables(dotEnvFiles ...string) []IEnvironmentVariable // GetFilesystem returns the filesystem associated with the current environment GetFilesystem() filesystem.FS + // GetEnvrionmentVariable returns the fetched environment variable or an error if it not set. optionally search .env files too + GetEnvironmentVariable(envvar string, dotEnvFiles ...string) (IEnvironmentVariable, error) } diff --git a/utils/mocks/mock_environment.go b/utils/mocks/mock_environment.go index af86905acd..85bc8474b2 100644 --- a/utils/mocks/mock_environment.go +++ b/utils/mocks/mock_environment.go @@ -8,10 +8,9 @@ import ( user "os/user" reflect "reflect" - gomock "github.com/golang/mock/gomock" - environment "github.com/ARM-software/golang-utils/utils/environment" filesystem "github.com/ARM-software/golang-utils/utils/filesystem" + gomock "github.com/golang/mock/gomock" ) // MockIEnvironmentVariable is a mock of IEnvironmentVariable interface. @@ -173,18 +172,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. From d6f72b961888140853ea2dd1e3e2dd3edd9e4988 Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Tue, 10 Oct 2023 18:38:30 +0100 Subject: [PATCH 2/3] :book: improved documentation --- changes/20231010151706.feature | 2 +- changes/20231010151811.feature | 2 +- utils/environment/current.go | 5 +++-- utils/environment/interfaces.go | 6 ++++-- utils/mocks/mock_environment.go | 3 ++- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/changes/20231010151706.feature b/changes/20231010151706.feature index 4b37c100d7..4a3404809b 100644 --- a/changes/20231010151706.feature +++ b/changes/20231010151706.feature @@ -1 +1 @@ -`environment` Add GetEnvironmentVariable() to try and fetch specific environment variable +:sparkles: [`environment`] Add `GetEnvironmentVariable()` to fetch a specific environment variable diff --git a/changes/20231010151811.feature b/changes/20231010151811.feature index e703d22f0e..2493c0a70a 100644 --- a/changes/20231010151811.feature +++ b/changes/20231010151811.feature @@ -1 +1 @@ -`environment` Add optional ...dotEnvFiles to GetEnvironmentVariables() and GetEnvironmentVariable() so that .env files can be loaded into the current environment +: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 ae3ee38820..b422e315b9 100644 --- a/utils/environment/current.go +++ b/utils/environment/current.go @@ -28,7 +28,8 @@ func (c *currentEnv) GetCurrentUser() (currentUser *user.User) { return } -// GetEnvironmentVariables returns the current environment variable (and optionally those in the supplied dotEnvFiles) +// 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 @@ -49,7 +50,7 @@ func (c *currentEnv) GetFilesystem() filesystem.FS { return filesystem.NewStandardFileSystem() } -// GetEnvironmentVariable searchs the current environment (and optionally dotEnvFiles) for a specific env var +// 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 { diff --git a/utils/environment/interfaces.go b/utils/environment/interfaces.go index 015335bac9..df474ef77d 100644 --- a/utils/environment/interfaces.go +++ b/utils/environment/interfaces.go @@ -30,10 +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 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 - // GetEnvrionmentVariable returns the fetched environment variable or an error if it not set. optionally search .env files too + // 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 85bc8474b2..1bf88700f5 100644 --- a/utils/mocks/mock_environment.go +++ b/utils/mocks/mock_environment.go @@ -8,9 +8,10 @@ import ( user "os/user" reflect "reflect" + gomock "github.com/golang/mock/gomock" + environment "github.com/ARM-software/golang-utils/utils/environment" filesystem "github.com/ARM-software/golang-utils/utils/filesystem" - gomock "github.com/golang/mock/gomock" ) // MockIEnvironmentVariable is a mock of IEnvironmentVariable interface. From 34f47234c9fca08b4ec1876c715f40232afe7a09 Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Tue, 10 Oct 2023 18:42:35 +0100 Subject: [PATCH 3/3] linting --- utils/environment/current_test.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/utils/environment/current_test.go b/utils/environment/current_test.go index 211fcde23a..b2af4b06c8 100644 --- a/utils/environment/current_test.go +++ b/utils/environment/current_test.go @@ -5,12 +5,13 @@ import ( "os" "testing" - "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" "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 ( @@ -47,7 +48,8 @@ func Test_currentEnv_GetEnvironmentVariables(t *testing.T) { require.NoError(t, err) defer func() { _ = dotenv1.Close() }() test3 := NewEnvironmentVariable("test3", faker.Sentence()) - dotenv1.WriteString(test3.String()) + _, err = dotenv1.WriteString(test3.String()) + require.NoError(t, err) err = dotenv1.Close() require.NoError(t, err) @@ -55,9 +57,11 @@ func Test_currentEnv_GetEnvironmentVariables(t *testing.T) { require.NoError(t, err) defer func() { _ = dotenv2.Close() }() test4 := NewEnvironmentVariable("test4", faker.Sentence()) - dotenv2.WriteString(fmt.Sprintf("%v\n", test4.String())) + _, err = dotenv2.WriteString(fmt.Sprintf("%v\n", test4.String())) + require.NoError(t, err) test5 := NewEnvironmentVariable("test5", faker.Sentence()) - dotenv2.WriteString(fmt.Sprintf("%v\n", test5.String())) + _, err = dotenv2.WriteString(fmt.Sprintf("%v\n", test5.String())) + require.NoError(t, err) err = dotenv2.Close() require.NoError(t, err) @@ -106,7 +110,8 @@ func Test_currentenv_GetEnvironmentVariable(t *testing.T) { require.NoError(t, err) defer func() { _ = dotenv1.Close() }() test3 := NewEnvironmentVariable("test3", faker.Sentence()) - dotenv1.WriteString(test3.String()) + _, err = dotenv1.WriteString(test3.String()) + require.NoError(t, err) err = dotenv1.Close() require.NoError(t, err) @@ -114,9 +119,11 @@ func Test_currentenv_GetEnvironmentVariable(t *testing.T) { require.NoError(t, err) defer func() { _ = dotenv2.Close() }() test4 := NewEnvironmentVariable("test4", faker.Sentence()) - dotenv2.WriteString(fmt.Sprintf("%v\n", test4.String())) + _, err = dotenv2.WriteString(fmt.Sprintf("%v\n", test4.String())) + require.NoError(t, err) test5 := NewEnvironmentVariable("test5", faker.Sentence()) - dotenv2.WriteString(fmt.Sprintf("%v\n", test5.String())) + _, err = dotenv2.WriteString(fmt.Sprintf("%v\n", test5.String())) + require.NoError(t, err) err = dotenv2.Close() require.NoError(t, err)