diff --git a/changes/20231013160208.feature b/changes/20231013160208.feature new file mode 100644 index 0000000000..c646a660bd --- /dev/null +++ b/changes/20231013160208.feature @@ -0,0 +1 @@ +:sparkles: `[environment]` Expose utilities for parsing and finding environment variables diff --git a/changes/20231013160247.bugfix b/changes/20231013160247.bugfix new file mode 100644 index 0000000000..ed9ba3a278 --- /dev/null +++ b/changes/20231013160247.bugfix @@ -0,0 +1 @@ +:bug: `[environment]` Fix environment variables parsing when an entry is incorrect diff --git a/utils/environment/current.go b/utils/environment/current.go index b422e315b9..27ee552c0f 100644 --- a/utils/environment/current.go +++ b/utils/environment/current.go @@ -1,14 +1,12 @@ 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" ) @@ -35,14 +33,7 @@ func (c *currentEnv) GetEnvironmentVariables(dotEnvFiles ...string) (variables [ _ = 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]) - if err != nil { - return - } - variables = append(variables, envvar) - } + variables = ParseEnvironmentVariables(os.Environ()...) return } @@ -52,13 +43,7 @@ func (c *currentEnv) GetFilesystem() filesystem.FS { // 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) + return FindEnvironmentVariable(envvar, c.GetEnvironmentVariables(dotEnvFiles...)...) } // NewCurrentEnvironment returns system current environment. diff --git a/utils/environment/envvar.go b/utils/environment/envvar.go index 366c5c0ba6..fce88e6711 100644 --- a/utils/environment/envvar.go +++ b/utils/environment/envvar.go @@ -82,10 +82,18 @@ func isEnvVarKey(value string) bool { // ParseEnvironmentVariable parses an environment variable definition, in the form "key=value". func ParseEnvironmentVariable(variable string) (IEnvironmentVariable, error) { elements := strings.Split(strings.TrimSpace(variable), "=") - if len(elements) != 2 { - return nil, fmt.Errorf("%w: invalid environment variable entry", commonerrors.ErrInvalid) + if len(elements) < 2 { + return nil, fmt.Errorf("%w: invalid environment variable entry as not following key=value", commonerrors.ErrInvalid) } - envvar := NewEnvironmentVariable(elements[0], elements[1]) + value := elements[1] + if len(elements) > 2 { + var valueElems []string + for i := 1; i < len(elements); i++ { + valueElems = append(valueElems, elements[i]) + } + value = strings.Join(valueElems, "=") + } + envvar := NewEnvironmentVariable(elements[0], value) return envvar, envvar.Validate() } @@ -114,3 +122,26 @@ func ValidateEnvironmentVariables(vars ...IEnvironmentVariable) error { } return nil } + +// ParseEnvironmentVariables parses a list of key=value entries such as os.Environ() and returns a list of the corresponding environment variables. +// Any entry failing parsing will be ignored. +func ParseEnvironmentVariables(variables ...string) (envVars []IEnvironmentVariable) { + for i := range variables { + envvar, err := ParseEnvironmentVariable(variables[i]) + if err != nil { + continue + } + envVars = append(envVars, envvar) + } + return +} + +// FindEnvironmentVariable looks for an environment variable in a list. if no environment variable matches, an error is returned +func FindEnvironmentVariable(envvar string, envvars ...IEnvironmentVariable) (IEnvironmentVariable, error) { + 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) +} diff --git a/utils/environment/envvar_test.go b/utils/environment/envvar_test.go index f1e4466315..26f3501e65 100644 --- a/utils/environment/envvar_test.go +++ b/utils/environment/envvar_test.go @@ -128,6 +128,10 @@ func TestParseEnvironmentVariable(t *testing.T) { errortest.AssertError(t, err, commonerrors.ErrInvalid) _, err = ParseEnvironmentVariable(faker.Word() + "=" + faker.Word()) require.NoError(t, err) + _, err = ParseEnvironmentVariable(faker.Word() + "=" + faker.Word() + "=") + require.NoError(t, err) + _, err = ParseEnvironmentVariable(faker.Word() + "=" + faker.Word() + "=" + faker.Sentence()) + require.NoError(t, err) key := strings.ReplaceAll(strings.ReplaceAll(faker.Sentence(), " ", "_"), ".", "") value := faker.Sentence() envTest := NewEnvironmentVariable(key, value) @@ -144,3 +148,16 @@ func TestParseEnvironmentVariable(t *testing.T) { require.NoError(t, env.UnmarshalText(txt)) assert.True(t, envTest.Equal(env)) } + +func TestEnvVar_ParseEnvironmentVariables(t *testing.T) { + username := faker.Username() + entries := []string{"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/65357/bus", "HOME=/home/josjen01", faker.UUIDHyphenated(), "EDITOR=hx", "LOGNAME=josjen01", "DISPLAY=:0", "SSH_AUTH_SOCK=/tmp/ssh-eBrdhiWnaFYp/agent.4969", "KRB5CCNAME=FILE:/tmp/krb5cc_65357_XLwjEE", "GPG_AGENT_INFO=/run/user/65357/gnupg/S.gpg-agent:0:1", "LANGUAGE=en_US:", "USER=" + username, "XDG_RUNTIME_DIR=/run/user/65357", "WINDOWID=54525966", "KITTY_PID=151539", "CMSIS_PACK_ROOT=/home/josjen01/.cache/arm/packs", "XDG_SESSION_ID=4", "XDG_CONFIG_DIRS=/etc/xdg/xdg-i3:/etc/xdg", faker.Name(), "GDMSESSION=i3", "WINDOWPATH=2", "SHLVL=1", "DESKTOP_SESSION=i3", "GTK_MODULES=gail:atk-bridge", "LANG=en_US.UTF-8", "FZF_DEFAULT_OPTS=--color dark,hl:#d65d08,hl+:#d65d08,fg+:#282828,bg+:#282828,fg+:#b58900,info:#ebdbb2,prompt:#268bd2,pointer:#2aa198,marker:#d33682,spinner:#268bd2 -m", "XDG_SESSION_DESKTOP=i3", "XDG_SESSION_TYPE=x11", "KITTY_PUBLIC_KEY=1:^1R-7)Aw|}io+D^KqaYVJF0R&a!f&dpX}gSSEIH&", "XDG_SEAT=seat0", "TERM=xterm-kitty", "XDG_DATA_DIRS=/usr/share/i3:/usr/local/share/:/usr/share/:/var/lib/snapd/desktop", "DESKTOP_STARTUP_ID=i3/kitty/4969-5-e126332_TIME31895328", "SHELL=/bin/bash", "KITTY_WINDOW_ID=6", "QT_ACCESSIBILITY=1", "COLORTERM=truecolor", "TERMINFO=/home/josjen01/.local/kitty.app/lib/kitty/terminfo", "SSH_AGENT_PID=5033", "XDG_SESSION_CLASS=user", "KITTY_INSTALLATION_DIR=/home/josjen01/.local/kitty.app/lib/kitty", "XDG_CURRENT_DESKTOP=i3", "MANPATH=:/home/josjen01/.local/kitty.app/share/man::/opt/puppetlabs/puppet/share/man", "XAUTHORITY=/run/user/65357/gdm/Xauthority", "CMSIS_COMPILER_ROOT=/etc/cmsis-build", "XDG_VTNR=2", "I3SOCK=/run/user/65357/i3/ipc-socket.4969", "USERNAME=josjen01", "PATH=/usr/local/go/bin:/bin:/home/josjen01/.local/bin:/home/josjen01/go/bin:/home/josjen01/.cargo/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/usr/games:/usr/local/games:/snap/bin:/opt/puppetlabs/bin", "PWD=/home/josjen01/Git/golang-utils/utils/environment", "GOCOVERDIR=/tmp/go-build1026478377/b001/gocoverdir", "test1=Accusantium voluptatem aut sit perferendis consequatur", "test2=Perferendis aut accusantium voluptatem sit consequatur.", faker.Word()} + environmentVariables := ParseEnvironmentVariables(entries...) + assert.NotEmpty(t, environmentVariables) + assert.Len(t, environmentVariables, len(entries)-3) + env, err := FindEnvironmentVariable("USER", environmentVariables...) + require.NoError(t, err) + assert.Equal(t, username, env.GetValue()) + _, err = FindEnvironmentVariable("TEST1", environmentVariables...) + errortest.AssertError(t, err, commonerrors.ErrNotFound) +}