Skip to content

Commit 21d1317

Browse files
committed
✨ Various new features
1 parent 81e3c2a commit 21d1317

File tree

17 files changed

+409
-27
lines changed

17 files changed

+409
-27
lines changed

.secrets.baseline

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,28 +123,28 @@
123123
"filename": "utils/config/service_configuration_test.go",
124124
"hashed_secret": "ddcec2f503a5d58f432a0beee3fb9544fa581f54",
125125
"is_verified": false,
126-
"line_number": 33
126+
"line_number": 35
127127
},
128128
{
129129
"type": "Secret Keyword",
130130
"filename": "utils/config/service_configuration_test.go",
131131
"hashed_secret": "7ca1cc114e7e5f955880bb96a5bf391b4dc20ab6",
132132
"is_verified": false,
133-
"line_number": 497
133+
"line_number": 533
134134
},
135135
{
136136
"type": "Secret Keyword",
137137
"filename": "utils/config/service_configuration_test.go",
138138
"hashed_secret": "11519c144be4850d95b34220a40030cbd5a36b57",
139139
"is_verified": false,
140-
"line_number": 592
140+
"line_number": 628
141141
},
142142
{
143143
"type": "Secret Keyword",
144144
"filename": "utils/config/service_configuration_test.go",
145145
"hashed_secret": "15fae91d8fa7f2c531c1cf3ddc745e1f4473c02d",
146146
"is_verified": false,
147-
"line_number": 599
147+
"line_number": 635
148148
}
149149
],
150150
"utils/filesystem/filehash_test.go": [
@@ -272,5 +272,5 @@
272272
}
273273
]
274274
},
275-
"generated_at": "2025-07-31T17:26:46Z"
275+
"generated_at": "2025-08-08T23:03:50Z"
276276
}

changes/20250808203905.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: [parallelisation] Added a way to register cancel functions to a close store to ensure everything is cancelled on close

changes/20250808204006.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: [commonerrors] Added a Join which is similar to errors.Join but following inline string convention

changes/20250808204222.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: [keyring] Added a module to store configuration in [system keyring service](https://github.com/zalando/go-keyring)

changes/20250808230944.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:sparkles: `[config]` Added ability to read configuration from system keyring service

utils/commonerrors/errors.go

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -247,32 +247,56 @@ func isSpecialCase(target, specialErrorCase error, prefix string) bool {
247247
return strings.TrimSuffix(target.Error(), underlyingErr.Error()) == prefix
248248
}
249249

250+
// Join is similar to errors.Join but follows the common errors convention when printing
251+
func Join(errs ...error) error {
252+
switch len(errs) {
253+
case 0:
254+
return nil
255+
case 1:
256+
return errs[0]
257+
default:
258+
err := multierror.Append(errs[0], errs[1:]...)
259+
err.ErrorFormat = func(e []error) string {
260+
builder := strings.Builder{}
261+
_, _ = builder.WriteString(e[0].Error())
262+
for i := range e[1:] {
263+
if None(e[i+1], nil) {
264+
_, _ = builder.WriteString(string(TypeReasonErrorSeparator))
265+
_, _ = builder.WriteString(" ")
266+
_, _ = builder.WriteString(e[i+1].Error())
267+
}
268+
}
269+
return builder.String()
270+
}
271+
return err.ErrorOrNil()
272+
}
273+
274+
}
275+
250276
// MarkAsFailure will tent an error as failure. It will retain its original error type but IsFailure should return true.
251277
func MarkAsFailure(err error) error {
252278
if Any(err, nil, ErrFailed) {
253279
return err
254280
}
255-
result := multierror.Append(err, ErrFailed)
256-
result.ErrorFormat = func(e []error) string {
257-
builder := strings.Builder{}
258-
_, _ = builder.WriteString(failureStr)
259-
for i := range e {
260-
if None(e[i], nil, ErrFailed) {
261-
_, _ = builder.WriteString(string(TypeReasonErrorSeparator))
262-
_, _ = builder.WriteString(" ")
263-
_, _ = builder.WriteString(e[i].Error())
264-
}
265-
}
266-
return builder.String()
267-
}
268-
return result.ErrorOrNil()
281+
return Join(ErrFailed, err)
269282
}
270283

271284
// NewFailure creates a failure object.
272285
func NewFailure(msgFormat string, args ...any) error {
286+
if len(args) == 0 {
287+
return New(ErrFailed, msgFormat)
288+
}
273289
return Newf(ErrFailed, msgFormat, args...)
274290
}
275291

292+
// NewWarningMessage creates a warning message.
293+
func NewWarningMessage(msgFormat string, args ...any) error {
294+
if len(args) == 0 {
295+
return New(ErrWarning, msgFormat)
296+
}
297+
return Newf(ErrWarning, msgFormat, args...)
298+
}
299+
276300
// NewWarning will create a warning wrapper around an existing commonerror so that it can be easily recovered. If the
277301
// underlying error is not a commonerror then ok will be set to false
278302
func NewWarning(target error) (ok bool, err error) {
@@ -357,7 +381,7 @@ func WrapError(targetError, originalError error, msg string) error {
357381
tErr = ErrUnknown
358382
}
359383
origErr := ConvertContextError(originalError)
360-
if Any(origErr, ErrTimeout, ErrCancelled, ErrWarning, ErrFailed) {
384+
if Any(origErr, ErrTimeout, ErrCancelled, ErrWarning, ErrFailed) || IsWarning(origErr) || IsFailure(origErr) {
361385
tErr = origErr
362386
}
363387
if originalError == nil {

utils/commonerrors/errors_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ func TestIgnoreCorrespondTo(t *testing.T) {
5959
assert.NoError(t, IgnoreCorrespondTo(ErrCondition, "condition"))
6060
}
6161

62+
func TestJoin(t *testing.T) {
63+
assert.True(t, Any(Join(ErrFailed, ErrMarshalling, ErrCancelled), ErrFailed))
64+
assert.True(t, Any(Join(ErrFailed, ErrMarshalling, ErrCancelled), ErrCancelled))
65+
assert.False(t, Any(Join(ErrFailed, ErrMarshalling, nil, ErrCancelled), nil))
66+
assert.True(t, IsWarning(Join(ErrFailed, ErrMarshalling, NewWarningMessage(faker.Sentence()), ErrCancelled)))
67+
require.NoError(t, Join(nil, nil, nil))
68+
}
69+
6270
func TestContextErrorConversion(t *testing.T) {
6371
defer goleak.VerifyNone(t)
6472
task := func(ctx context.Context) {
@@ -126,6 +134,7 @@ func TestIsCommonError(t *testing.T) {
126134

127135
func TestIsWarning(t *testing.T) {
128136
assert.True(t, IsWarning(ErrWarning))
137+
assert.True(t, IsWarning(NewWarningMessage(faker.Sentence())))
129138
assert.False(t, IsWarning(ErrUnexpected))
130139
assert.False(t, IsWarning(nil))
131140
assert.True(t, IsWarning(fmt.Errorf("%w: i am a warning", ErrWarning)))

utils/config/service_configuration.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package config
77

88
import (
9+
"context"
910
"fmt"
1011
"strings"
1112

@@ -17,6 +18,7 @@ import (
1718
"github.com/ARM-software/golang-utils/utils/collection"
1819
"github.com/ARM-software/golang-utils/utils/commonerrors"
1920
"github.com/ARM-software/golang-utils/utils/field"
21+
"github.com/ARM-software/golang-utils/utils/keyring"
2022
"github.com/ARM-software/golang-utils/utils/reflection"
2123
)
2224

@@ -35,6 +37,16 @@ func Load(envVarPrefix string, configurationToSet IServiceConfiguration, default
3537
return LoadFromViper(viper.New(), envVarPrefix, configurationToSet, defaultConfiguration)
3638
}
3739

40+
// LoadFromSystem is similar to Load but also fetches values from system's [keyring service](https://github.com/zalando/go-keyring).
41+
func LoadFromSystem(envVarPrefix string, configurationToSet IServiceConfiguration, defaultConfiguration IServiceConfiguration) error {
42+
return LoadFromViperAndSystem(viper.New(), envVarPrefix, configurationToSet, defaultConfiguration)
43+
}
44+
45+
// LoadFromViperAndSystem is the same as `LoadFromViper` but also fetches values from system's [keyring service](https://github.com/zalando/go-keyring).
46+
func LoadFromViperAndSystem(viperSession *viper.Viper, envVarPrefix string, configurationToSet IServiceConfiguration, defaultConfiguration IServiceConfiguration) error {
47+
return LoadFromEnvironmentAndSystem(viperSession, envVarPrefix, configurationToSet, defaultConfiguration, "", true)
48+
}
49+
3850
// LoadFromViper is the same as `Load` but instead of creating a new viper session, reuse the one provided.
3951
// Important note:
4052
// Viper's precedence order is maintained:
@@ -58,7 +70,22 @@ func LoadFromViper(viperSession *viper.Viper, envVarPrefix string, configuration
5870
// 5) key/value store
5971
// 6) default values (set via flag default values, or calls to `SetDefault` or via `defaultConfiguration` argument provided)
6072
// Nonetheless, when it comes to default values. It differs slightly from Viper as default values from the default Configuration (i.e. `defaultConfiguration` argument provided) will take precedence over defaults set via `SetDefault` or flags unless they are considered empty values according to `reflection.IsEmpty`.
61-
func LoadFromEnvironment(viperSession *viper.Viper, envVarPrefix string, configurationToSet IServiceConfiguration, defaultConfiguration IServiceConfiguration, configFile string) (err error) {
73+
func LoadFromEnvironment(viperSession *viper.Viper, envVarPrefix string, configurationToSet IServiceConfiguration, defaultConfiguration IServiceConfiguration, configFile string) error {
74+
return LoadFromEnvironmentAndSystem(viperSession, envVarPrefix, configurationToSet, defaultConfiguration, configFile, false)
75+
}
76+
77+
// LoadFromEnvironmentAndSystem is the same as `LoadFromEnvironment` but also gives the ability to load the configuration from system's [keyring service](https://github.com/zalando/go-keyring).
78+
// Important note:
79+
// Viper's precedence order is mostly maintained:
80+
// 1) values defined in keyring (if not empty and keyring is selected - this is the only difference from Viper)
81+
// 2) values set using explicit calls to `Set`
82+
// 3) flags
83+
// 4) environment (variables or `.env`)
84+
// 5) configuration file
85+
// 6) key/value store
86+
// 7) default values (set via flag default values, or calls to `SetDefault` or via `defaultConfiguration` argument provided)
87+
// Nonetheless, when it comes to default values. It differs slightly from Viper as default values from the default Configuration (i.e. `defaultConfiguration` argument provided) will take precedence over defaults set via `SetDefault` or flags unless they are considered empty values according to `reflection.IsEmpty`.
88+
func LoadFromEnvironmentAndSystem(viperSession *viper.Viper, envVarPrefix string, configurationToSet IServiceConfiguration, defaultConfiguration IServiceConfiguration, configFile string, useKeyring bool) (err error) {
6289
// Load Defaults
6390
var defaults map[string]interface{}
6491
err = mapstructure.Decode(defaultConfiguration, &defaults)
@@ -91,6 +118,12 @@ func LoadFromEnvironment(viperSession *viper.Viper, envVarPrefix string, configu
91118
err = commonerrors.WrapError(commonerrors.ErrMarshalling, err, "unable to fill configuration structure from the configuration session")
92119
return
93120
}
121+
if useKeyring {
122+
err = commonerrors.Ignore(keyring.FetchPointer[IServiceConfiguration](context.Background(), envVarPrefix, configurationToSet), commonerrors.ErrUnsupported)
123+
if err != nil {
124+
return
125+
}
126+
}
94127
// Run validation
95128
err = WrapValidationError(field.ToOptionalString(envVarPrefix), configurationToSet.Validate())
96129
return

utils/config/service_configuration_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package config
66

77
import (
8+
"context"
89
"errors"
910
"fmt"
1011
"math/rand"
@@ -22,6 +23,7 @@ import (
2223

2324
"github.com/ARM-software/golang-utils/utils/commonerrors"
2425
"github.com/ARM-software/golang-utils/utils/commonerrors/errortest"
26+
"github.com/ARM-software/golang-utils/utils/keyring"
2527
)
2628

2729
var (
@@ -125,6 +127,7 @@ func TestServiceConfigurationLoad(t *testing.T) {
125127
os.Clearenv()
126128
configTest := &ConfigurationTest{}
127129
defaults := DefaultConfiguration()
130+
require.NoError(t, keyring.Clear(context.Background(), "test"))
128131
err := Load("test", configTest, defaults)
129132
// Some required values are missing.
130133
require.Error(t, err)
@@ -171,6 +174,39 @@ func TestServiceConfigurationLoad(t *testing.T) {
171174
assert.NotEqual(t, expectedDB, configTest.TestConfig.DB)
172175
assert.True(t, configTest.TestConfig.Flag)
173176
assert.False(t, configTest.TestConfig2.Flag)
177+
t.Run("load from system", func(t *testing.T) {
178+
configTest2 := &ConfigurationTest{}
179+
err = LoadFromSystem("test", configTest2, defaults)
180+
require.NoError(t, err)
181+
require.NoError(t, configTest2.Validate())
182+
})
183+
t.Run("load from system", func(t *testing.T) {
184+
configTest2 := &ConfigurationTest{}
185+
err = Load("test", configTest2, defaults)
186+
require.NoError(t, err)
187+
require.NoError(t, configTest2.Validate())
188+
assert.EqualExportedValues(t, configTest, configTest2)
189+
configTest2.TestConfig2.Host = faker.URL()
190+
configTest2.TestConfig2.User = faker.Name()
191+
assert.NotEqual(t, configTest, configTest2)
192+
err := keyring.Store[ConfigurationTest](context.Background(), "test", configTest2)
193+
errortest.AssertError(t, err, nil, commonerrors.ErrUnsupported)
194+
if commonerrors.Any(err, commonerrors.ErrUnsupported) {
195+
t.Skip("keyring is not supported")
196+
}
197+
configTest3 := &ConfigurationTest{}
198+
err = LoadFromSystem("test", configTest3, defaults)
199+
require.NoError(t, err)
200+
require.NoError(t, configTest3.Validate())
201+
assert.EqualExportedValues(t, configTest2, configTest3)
202+
assert.NotEqual(t, configTest, configTest3)
203+
configTest4 := &ConfigurationTest{}
204+
err = Load("test", configTest4, defaults)
205+
require.NoError(t, err)
206+
require.NoError(t, configTest4.Validate())
207+
assert.EqualExportedValues(t, configTest, configTest4)
208+
assert.NotEqual(t, configTest4, configTest3)
209+
})
174210
}
175211

176212
func TestServiceConfigurationLoad_Errors(t *testing.T) {

utils/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ require (
4141
github.com/spf13/pflag v1.0.7
4242
github.com/spf13/viper v1.20.1
4343
github.com/stretchr/testify v1.10.0
44+
github.com/zalando/go-keyring v0.2.6
4445
go.uber.org/atomic v1.11.0
4546
go.uber.org/goleak v1.3.0
4647
go.uber.org/mock v0.5.2
@@ -56,12 +57,14 @@ require (
5657
)
5758

5859
require (
60+
al.essio.dev/pkg/shellescape v1.5.1 // indirect
5961
dario.cat/mergo v1.0.0 // indirect
6062
github.com/Microsoft/go-winio v0.6.2 // indirect
6163
github.com/ProtonMail/go-crypto v1.1.6 // indirect
6264
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 // indirect
6365
github.com/cloudflare/circl v1.6.1 // indirect
6466
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
67+
github.com/danieljoos/wincred v1.2.2 // indirect
6568
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
6669
github.com/dmarkham/enumer v1.5.11 // indirect
6770
github.com/ebitengine/purego v0.8.4 // indirect
@@ -71,6 +74,7 @@ require (
7174
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
7275
github.com/go-git/go-billy/v5 v5.6.2 // indirect
7376
github.com/go-ole/go-ole v1.2.6 // indirect
77+
github.com/godbus/dbus/v5 v5.1.0 // indirect
7478
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
7579
github.com/google/cabbie v1.0.2 // indirect
7680
github.com/google/glazier v0.0.0-20211029225403-9f766cca891d // indirect

0 commit comments

Comments
 (0)