Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[config] Added a way to bind mutiple flags to a same environment input: BindFlagsToEnv #519

Merged
merged 2 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,6 @@
{
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
},
{
"path": "detect_secrets.filters.common.is_baseline_file",
"filename": ".secrets.baseline"
},
{
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
"min_level": 2
Expand Down Expand Up @@ -126,28 +122,28 @@
"filename": "utils/config/service_configuration_test.go",
"hashed_secret": "ddcec2f503a5d58f432a0beee3fb9544fa581f54",
"is_verified": false,
"line_number": 32
"line_number": 33
},
{
"type": "Secret Keyword",
"filename": "utils/config/service_configuration_test.go",
"hashed_secret": "7ca1cc114e7e5f955880bb96a5bf391b4dc20ab6",
"is_verified": false,
"line_number": 369
"line_number": 481
},
{
"type": "Secret Keyword",
"filename": "utils/config/service_configuration_test.go",
"hashed_secret": "11519c144be4850d95b34220a40030cbd5a36b57",
"is_verified": false,
"line_number": 464
"line_number": 576
},
{
"type": "Secret Keyword",
"filename": "utils/config/service_configuration_test.go",
"hashed_secret": "15fae91d8fa7f2c531c1cf3ddc745e1f4473c02d",
"is_verified": false,
"line_number": 471
"line_number": 583
}
],
"utils/filesystem/filehash_test.go": [
Expand Down Expand Up @@ -268,5 +264,5 @@
}
]
},
"generated_at": "2024-08-14T13:57:27Z"
"generated_at": "2024-11-18T17:37:06Z"
}
1 change: 1 addition & 0 deletions changes/20241118165828.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: `[collection]` Added a way to determine unique values in a slice: `UniqueEntries`
1 change: 1 addition & 0 deletions changes/20241118171703.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: `[config]` Added a way to bind mutiple flag to a same environment input: `BindFlagsToEnv`
1 change: 1 addition & 0 deletions changes/20241118172847.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:arrow_up: Upgrade dependency `https://github.com/deckarep/golang-set`
15 changes: 13 additions & 2 deletions utils/collection/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
*/
package collection

import "strings"
import (
"strings"

mapset "github.com/deckarep/golang-set/v2"
)

// Find looks for an element in a slice. If found it will
// return its index and true; otherwise it will return -1 and false.
Expand All @@ -16,7 +20,7 @@ func Find(slice *[]string, val string) (int, bool) {
}

// FindInSlice finds if any values val are present in the slice and if so returns the first index.
// if strict, it check of an exact match; otherwise discard whitespaces and case.
// if strict, it checks for an exact match; otherwise it discards whitespaces and case.
func FindInSlice(strict bool, slice []string, val ...string) (int, bool) {
if len(val) == 0 || len(slice) == 0 {
return -1, false
Expand Down Expand Up @@ -46,6 +50,13 @@ func FindInSlice(strict bool, slice []string, val ...string) (int, bool) {
return -1, false
}

// UniqueEntries returns all the unique values contained in a slice.
func UniqueEntries[T comparable](slice []T) []T {
subSet := mapset.NewSet[T]()
_ = subSet.Append(slice...)
return subSet.ToSlice()
}

// Any returns true if there is at least one element of the slice which is true.
func Any(slice []bool) bool {
for i := range slice {
Expand Down
13 changes: 13 additions & 0 deletions utils/collection/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,16 @@ func TestAllNotEmpty(t *testing.T) {
assert.False(t, AllNotEmpty(false, []string{faker.Username(), "", faker.Name(), "", faker.Sentence()}))
assert.True(t, AllNotEmpty(false, []string{faker.Username(), faker.Name(), faker.Sentence()}))
}

func TestUniqueEntries(t *testing.T) {
assert.Len(t, UniqueEntries([]string{faker.Username(), faker.Name(), faker.Sentence(), faker.Name()}), 4)
values := UniqueEntries([]string{"test1", "test12", "test1", "test1", "test12", "test12"})
assert.Len(t, values, 2)
_, found := FindInSlice(true, values, "test1")
assert.True(t, found)
_, found = FindInSlice(true, values, "test12")
assert.True(t, found)

intValues := UniqueEntries([]int{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4})
assert.Len(t, intValues, 4)
}
103 changes: 103 additions & 0 deletions utils/config/service_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/spf13/pflag"
"github.com/spf13/viper"

"github.com/ARM-software/golang-utils/utils/collection"
"github.com/ARM-software/golang-utils/utils/commonerrors"
"github.com/ARM-software/golang-utils/utils/reflection"
)
Expand Down Expand Up @@ -136,6 +137,108 @@ func BindFlagToEnv(viperSession *viper.Viper, envVarPrefix string, envVar string
return
}

// BindFlagsToEnv binds a set of pflags to an environment variable.
// Envvar is the environment variable string with or without the prefix envVarPrefix
// It is similar to BindFlagToEnv but can be applied to multiple flags.
// Note: all the flags will have to be of the same type. If more than one flags is changed, the system will pick one at random.
func BindFlagsToEnv(viperSession *viper.Viper, envVarPrefix string, envVar string, flags ...*pflag.Flag) (err error) {

setEnvOptions(viperSession, envVarPrefix)
shortKey, cleansedEnvVar := generateEnvVarConfigKeys(envVar, envVarPrefix)

flagset, err := newMultiFlags(shortKey, flags...)
if err != nil {
return
}
err = viperSession.BindFlagValue(shortKey, flagset)
if err != nil {
return
}

err = viperSession.BindEnv(shortKey, cleansedEnvVar)
return
}

func newMultiFlags(name string, flags ...*pflag.Flag) (f viper.FlagValue, err error) {
if name == "" {
err = fmt.Errorf("%w: flag set must be associated with a name", commonerrors.ErrUndefined)
return
}
if len(flags) == 0 {
err = fmt.Errorf("%w: flags must be specified", commonerrors.ErrUndefined)
return
}
var fTypes []string
for i := range flags {
if flags[i] != nil {
fTypes = append(fTypes, flags[i].Value.Type())
}
}
fTypes = collection.UniqueEntries(fTypes)
if len(fTypes) != 1 {
err = fmt.Errorf("%w: flags in a set can only be of the same type: %v", commonerrors.ErrInvalid, fTypes)
return
}

f = &multiFlags{
commonName: name,
flags: flags,
}
return
}

type multiFlags struct {
commonName string
flags []*pflag.Flag
}

func (m *multiFlags) HasChanged() bool {
for i := range m.flags {
flag := m.flags[i]
if flag != nil && flag.Changed {
return true
}
}
return false
}

func (m *multiFlags) Name() string {
return m.commonName
}

func (m *multiFlags) ValueString() string {
var values []string
var firstValue string
for i := range m.flags {
flag := m.flags[i]
if flag != nil {
firstValue = flag.Value.String()
if flag.Changed {
values = append(values, flag.Value.String())
}
}
}
values = collection.UniqueEntries(values)
if len(values) >= 1 {
return values[0]
} else {
return firstValue
}
}

func (m *multiFlags) ValueType() string {
for i := range m.flags {
flag := m.flags[i]
if flag != nil {
vType := flag.Value.Type()
if vType != "" {
return vType
}
}
}
return ""
}

func generateEnvVarConfigKeys(envVar, envVarPrefix string) (shortKey string, cleansedEnvVar string) {
envVarLower := strings.ToLower(envVar)
envVarPrefixLower := strings.ToLower(envVarPrefix)
Expand Down
114 changes: 113 additions & 1 deletion utils/config/service_configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/ARM-software/golang-utils/utils/commonerrors"
"github.com/ARM-software/golang-utils/utils/commonerrors/errortest"
)

var (
Expand Down Expand Up @@ -291,6 +292,117 @@ func TestFlagBinding(t *testing.T) {
assert.False(t, configTest.TestConfig2.Flag)
}

func TestFlagsBinding(t *testing.T) {
os.Clearenv()
configTest := &ConfigurationTest{}
defaults := DefaultConfiguration()
session := viper.New()
var err error
flagSet := pflag.FlagSet{}
prefix := "test"
flagSet.String("host1", "a host", "dummy host")
flagSet.String("host2", "a host", "dummy host")
flagSet.String("password1", "a password1", "dummy password1")
flagSet.String("password2", "a password2", "dummy password2")
flagSet.String("password3", "a password3", "dummy password3")
flagSet.String("user", "a user", "dummy user")
flagSet.String("user1", "a user", "dummy user 1")
flagSet.String("user2", "a user", "dummy user 2")
flagSet.String("db", "a db", "dummy db")
flagSet.String("db2", "a db", "dummy db")
flagSet.Int("int", 0, "dummy int")
flagSet.Duration("time", time.Second, "dummy time")
flagSet.Bool("flag", false, "dummy flag")
err = BindFlagsToEnv(session, prefix, "TEST_DUMMYCONFIG_DUMMY_HOST", flagSet.Lookup("host2"), flagSet.Lookup("host2"))
require.NoError(t, err)
err = BindFlagsToEnv(session, prefix, "TEST_DUMMY_CONFIG_DUMMY_HOST", flagSet.Lookup("host1"), flagSet.Lookup("host2"))
require.NoError(t, err)
err = BindFlagsToEnv(session, prefix, "DUMMYCONFIG_PASSWORD", flagSet.Lookup("password2"), flagSet.Lookup("password3"))
require.NoError(t, err)
err = BindFlagsToEnv(session, prefix, "DUMMY_CONFIG_PASSWORD", flagSet.Lookup("password1"), flagSet.Lookup("password2"))
require.NoError(t, err)
err = BindFlagsToEnv(session, prefix, "DUMMYCONFIG_USER", flagSet.Lookup("user"), flagSet.Lookup("user1"), flagSet.Lookup("user2"))
require.NoError(t, err)
err = BindFlagsToEnv(session, prefix, "DUMMY_CONFIG_USER", flagSet.Lookup("user1"), flagSet.Lookup("user2"))
require.NoError(t, err)
err = BindFlagsToEnv(session, prefix, "TEST_DUMMYCONFIG_DB", flagSet.Lookup("db"))
require.NoError(t, err)
err = BindFlagsToEnv(session, prefix, "DUMMY_CONFIG_DB", flagSet.Lookup("db2"), flagSet.Lookup("db2"), flagSet.Lookup("db2"), flagSet.Lookup("db2"))
require.NoError(t, err)
err = BindFlagsToEnv(session, prefix, "DUMMY_CONFIG_FLAG", flagSet.Lookup("flag"), flagSet.Lookup("flag"), flagSet.Lookup("flag"))
require.NoError(t, err)
err = BindFlagsToEnv(session, prefix, "DUMMY_INT", flagSet.Lookup("int"), flagSet.Lookup("int"), flagSet.Lookup("int"), flagSet.Lookup("int"))
require.NoError(t, err)
err = BindFlagsToEnv(session, prefix, "DUMMY_Time", flagSet.Lookup("time"), flagSet.Lookup("time"), flagSet.Lookup("time"))
require.NoError(t, err)
err = flagSet.Set("host2", expectedHost)
require.NoError(t, err)
fvalue, err := flagSet.GetString("host2")
require.NoError(t, err)
assert.Equal(t, expectedHost, fvalue)
fvalue, err = flagSet.GetString("host1")
require.NoError(t, err)
assert.NotEqual(t, expectedHost, fvalue)
err = flagSet.Set("password2", expectedPassword)
require.NoError(t, err)
user1V := faker.Name()
err = flagSet.Set("user1", user1V)
require.NoError(t, err)
user2V := faker.Name()
err = flagSet.Set("user2", user2V)
require.NoError(t, err)
assert.NotEqual(t, user1V, user2V)
err = flagSet.Set("db", expectedDB) // Should take precedence over environment
require.NoError(t, err)
aDifferentDB := "another test db"
assert.NotEqual(t, expectedDB, aDifferentDB)
err = os.Setenv("TEST_DUMMY_CONFIG_DB", aDifferentDB)
require.NoError(t, err)
err = os.Setenv("TEST_DUMMYCONFIG_DB", aDifferentDB)
require.NoError(t, err)
err = flagSet.Set("int", fmt.Sprintf("%v", expectedInt))
require.NoError(t, err)
err = flagSet.Set("time", expectedDuration.String())
require.NoError(t, err)
err = flagSet.Set("flag", fmt.Sprintf("%v", false))
require.NoError(t, err)
flag, err := flagSet.GetBool("flag")
require.NoError(t, err)
assert.False(t, flag)
assert.False(t, session.GetBool("dummy.config.flag"))
err = LoadFromViper(session, prefix, configTest, defaults)
require.NoError(t, err)
require.NoError(t, configTest.Validate())
assert.Equal(t, expectedString, configTest.TestString)
assert.Equal(t, expectedInt, configTest.TestInt)
assert.Equal(t, expectedDuration, configTest.TestTime)
assert.Equal(t, defaults.TestConfig.Port, configTest.TestConfig.Port)
assert.Contains(t, []string{user1V, user2V}, configTest.TestConfig2.User)
assert.Equal(t, expectedHost, configTest.TestConfig.Host)
assert.Equal(t, expectedHost, configTest.TestConfig2.Host)
assert.Equal(t, expectedPassword, configTest.TestConfig.Password)
assert.Equal(t, expectedPassword, configTest.TestConfig2.Password)
assert.Equal(t, expectedDB, configTest.TestConfig.DB)
assert.Equal(t, aDifferentDB, configTest.TestConfig2.DB)
assert.NotEqual(t, expectedDB, configTest.TestConfig2.DB)
assert.True(t, configTest.TestConfig.Flag)
assert.False(t, configTest.TestConfig2.Flag)
}

func TestFlagsBindingErrors(t *testing.T) {
os.Clearenv()
session := viper.New()
flagSet := pflag.FlagSet{}
prefix := "test"
flagSet.String("db2", "a db", "dummy db")
flagSet.Int("int", 0, "dummy int")
err := BindFlagsToEnv(session, prefix, "TEST_DUMMYCONFIG_DUMMY_HOST")
errortest.AssertError(t, err, commonerrors.ErrUndefined)
err = BindFlagsToEnv(session, prefix, "TEST_DUMMYCONFIG_DUMMY_HOST", flagSet.Lookup("db2"), flagSet.Lookup("int"))
errortest.AssertError(t, err, commonerrors.ErrInvalid)

}

func TestFlagBindingDefaults(t *testing.T) {
os.Clearenv()
configTest := &ConfigurationTest{}
Expand Down Expand Up @@ -545,7 +657,7 @@ func Test_convertViperError(t *testing.T) {
for i := range tests {
t.Run(fmt.Sprintf("%v", i), func(t *testing.T) {
test := tests[i]
require.True(t, commonerrors.Any(convertViperError(test.viperErr), test.expectedError))
errortest.RequireError(t, convertViperError(test.viperErr), test.expectedError)
})
}
}
Expand Down
6 changes: 3 additions & 3 deletions utils/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"strings"
"sync"

mapset "github.com/deckarep/golang-set"
mapset "github.com/deckarep/golang-set/v2"
git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
Expand Down Expand Up @@ -44,7 +44,7 @@ type CloneObject struct {
repo *git.Repository
allEntries chan Entry

seen mapset.Set
seen mapset.Set[plumbing.Hash]

totalSize *atomic.Int64
trueSize *atomic.Int64
Expand Down Expand Up @@ -255,7 +255,7 @@ func NewCloneObject() *CloneObject {
processNonTreeOnly: atomic.NewBool(false),
treeSeenIdentifier: atomic.NewString(""),
nonTreeOnlyMutex: make(chan int, 1),
seen: mapset.NewSet(),
seen: mapset.NewSet[plumbing.Hash](),
}
cloneObject.nonTreeOnlyMutex <- 1
return cloneObject
Expand Down
Loading
Loading