From 2faffc628201b18e9767757d102cd1edde43b506 Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Tue, 14 Apr 2020 17:03:45 +0200 Subject: [PATCH 01/13] add text unmarshaler with test --- ssmconfig.go | 26 +++++++++++++++++++++++++- ssmconfig_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/ssmconfig.go b/ssmconfig.go index 20a5f8a..5feb2bc 100755 --- a/ssmconfig.go +++ b/ssmconfig.go @@ -3,6 +3,7 @@ package ssmconfig import ( + "encoding" "path" "reflect" "strconv" @@ -84,7 +85,13 @@ func (p *Provider) Process(configPath string, c interface{}) error { continue } - err = setValue(v.Field(i), value) + field := v.Field(i) + var err error + if isTextUnmarshaler(field) { + err = unmarshalText(field, value) + } else { + err = setValue(v.Field(i), value) + } if err != nil { return errors.Wrapf(err, "ssmconfig: error setting field %s", v.Type().Field(i).Name) } @@ -129,6 +136,23 @@ func (p *Provider) getParameters(spec structSpec) (params map[string]string, inv return params, invalidParams, nil } +func isTextUnmarshaler(v reflect.Value) bool { + tu := reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + return v.Type().Implements(tu) +} + +func unmarshalText(v reflect.Value, s string) error { + m := v.MethodByName("UnmarshalText") + v.Set(reflect.New(v.Type().Elem())) + res := m.Call([]reflect.Value{reflect.ValueOf([]byte(s))}) + if len(res) != 1 || !res[0].IsNil() { + return errors.Errorf("could not decode %q into type %v: %v", s, + v.Type().String(), res[0].Elem().Interface().(error)) + } + return nil + +} + func setValue(v reflect.Value, s string) error { switch v.Kind() { case reflect.String: diff --git a/ssmconfig_test.go b/ssmconfig_test.go index b09adba..7fd5f9c 100755 --- a/ssmconfig_test.go +++ b/ssmconfig_test.go @@ -1,6 +1,7 @@ package ssmconfig_test import ( + "encoding/hex" "errors" "reflect" "testing" @@ -18,6 +19,20 @@ type mockSSMClient struct { err error } +type Hex struct { + V string +} + +func (h *Hex) UnmarshalText(val []byte) error { + dst := make([]byte, hex.DecodedLen(len(val))) + n, err := hex.Decode(dst, val) + if err != nil { + return err + } + h.V = string(dst[:n]) + return nil +} + func (c *mockSSMClient) GetParameters(input *ssm.GetParametersInput) (*ssm.GetParametersOutput, error) { c.calledWithInput = input if c.err != nil { @@ -39,6 +54,8 @@ func TestProvider_Process(t *testing.T) { F322 float32 `ssm:"/float32/f322" default:"42.42"` F641 float64 `ssm:"/float64/f641"` F642 float64 `ssm:"/float64/f642" default:"42.42"` + H1 *Hex `ssm:"/hex/hex1"` + H2 *Hex `ssm:"/hex/hex2" default:"737472696e6731"` Invalid string } @@ -65,6 +82,10 @@ func TestProvider_Process(t *testing.T) { Name: aws.String("/base/float64/f641"), Value: aws.String("42.42"), }, + { + Name: aws.String("/base/hex/hex1"), + Value: aws.String("737472696e6731"), + }, }, }, } @@ -94,6 +115,8 @@ func TestProvider_Process(t *testing.T) { "/base/float32/f322", "/base/float64/f641", "/base/float64/f642", + "/base/hex/hex1", + "/base/hex/hex2", } if !reflect.DeepEqual(names, expectedNames) { @@ -130,6 +153,12 @@ func TestProvider_Process(t *testing.T) { if s.F642 != 42.42 { t.Errorf("Process() F642 unexpected value: want %f, have %f", 42.42, s.F642) } + if s.H1.V != "string1" { + t.Errorf("Process() H1 unexpected value: want %s, have %s", "string1", s.H1.V) + } + if s.H2.V != "string1" { + t.Errorf("Process() H2 unexpected value: want %s, have %s", "string1", s.H2.V) + } if s.Invalid != "" { t.Errorf("Process() Missing unexpected value: want %q, have %q", "", s.Invalid) } From 93991996c17359a436413eb71c8045cfbb214012 Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Tue, 14 Apr 2020 21:46:29 +0200 Subject: [PATCH 02/13] support non-pointer only and use time as example --- ssmconfig.go | 33 ++++++++++++++---------- ssmconfig_test.go | 66 ++++++++++++++++++++++------------------------- 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/ssmconfig.go b/ssmconfig.go index 5feb2bc..e2f3e71 100755 --- a/ssmconfig.go +++ b/ssmconfig.go @@ -60,7 +60,8 @@ func (p *Provider) Process(configPath string, c interface{}) error { return errors.New("ssmconfig: c must be a pointer to a struct") } - spec := buildStructSpec(configPath, v.Type()) + t := v.Type() + spec := buildStructSpec(configPath, t) params, invalidPrams, err := p.getParameters(spec) if err != nil { @@ -86,11 +87,12 @@ func (p *Provider) Process(configPath string, c interface{}) error { } field := v.Field(i) + structField := t.Field(i) var err error - if isTextUnmarshaler(field) { - err = unmarshalText(field, value) + if isTextUnmarshaler(structField) { + err = unmarshalText(structField, field, value) } else { - err = setValue(v.Field(i), value) + err = setValue(field, value) } if err != nil { return errors.Wrapf(err, "ssmconfig: error setting field %s", v.Type().Field(i).Name) @@ -136,21 +138,26 @@ func (p *Provider) getParameters(spec structSpec) (params map[string]string, inv return params, invalidParams, nil } -func isTextUnmarshaler(v reflect.Value) bool { +func isTextUnmarshaler(f reflect.StructField) bool { tu := reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() - return v.Type().Implements(tu) + return reflect.PtrTo(f.Type).Implements(tu) } -func unmarshalText(v reflect.Value, s string) error { - m := v.MethodByName("UnmarshalText") - v.Set(reflect.New(v.Type().Elem())) - res := m.Call([]reflect.Value{reflect.ValueOf([]byte(s))}) +func unmarshalText(f reflect.StructField, v reflect.Value, s string) error { + newV := reflect.New(f.Type) + + m := newV.MethodByName("UnmarshalText") + args := []reflect.Value{reflect.ValueOf([]byte(s))} + res := m.Call(args) + if len(res) != 1 || !res[0].IsNil() { - return errors.Errorf("could not decode %q into type %v: %v", s, - v.Type().String(), res[0].Elem().Interface().(error)) + err := res[0].Elem().Interface().(error) + return errors.Errorf("could not decode %q into type %v: %v", s, f.Type.String(), err) } - return nil + v.Set(newV.Elem()) + + return nil } func setValue(v reflect.Value, s string) error { diff --git a/ssmconfig_test.go b/ssmconfig_test.go index 7fd5f9c..d8a998b 100755 --- a/ssmconfig_test.go +++ b/ssmconfig_test.go @@ -1,10 +1,10 @@ package ssmconfig_test import ( - "encoding/hex" "errors" "reflect" "testing" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ssm" @@ -19,20 +19,6 @@ type mockSSMClient struct { err error } -type Hex struct { - V string -} - -func (h *Hex) UnmarshalText(val []byte) error { - dst := make([]byte, hex.DecodedLen(len(val))) - n, err := hex.Decode(dst, val) - if err != nil { - return err - } - h.V = string(dst[:n]) - return nil -} - func (c *mockSSMClient) GetParameters(input *ssm.GetParametersInput) (*ssm.GetParametersOutput, error) { c.calledWithInput = input if c.err != nil { @@ -44,18 +30,18 @@ func (c *mockSSMClient) GetParameters(input *ssm.GetParametersInput) (*ssm.GetPa func TestProvider_Process(t *testing.T) { t.Run("base case", func(t *testing.T) { var s struct { - S1 string `ssm:"/strings/s1"` - S2 string `ssm:"/strings/s2" default:"string2"` - I1 int `ssm:"/int/i1"` - I2 int `ssm:"/int/i2" default:"42"` - B1 bool `ssm:"/bool/b1"` - B2 bool `ssm:"/bool/b2" default:"false"` - F321 float32 `ssm:"/float32/f321"` - F322 float32 `ssm:"/float32/f322" default:"42.42"` - F641 float64 `ssm:"/float64/f641"` - F642 float64 `ssm:"/float64/f642" default:"42.42"` - H1 *Hex `ssm:"/hex/hex1"` - H2 *Hex `ssm:"/hex/hex2" default:"737472696e6731"` + S1 string `ssm:"/strings/s1"` + S2 string `ssm:"/strings/s2" default:"string2"` + I1 int `ssm:"/int/i1"` + I2 int `ssm:"/int/i2" default:"42"` + B1 bool `ssm:"/bool/b1"` + B2 bool `ssm:"/bool/b2" default:"false"` + F321 float32 `ssm:"/float32/f321"` + F322 float32 `ssm:"/float32/f322" default:"42.42"` + F641 float64 `ssm:"/float64/f641"` + F642 float64 `ssm:"/float64/f642" default:"42.42"` + TU1 time.Time `ssm:"/text_unmarshaler/time1"` + TU2 time.Time `ssm:"/text_unmarshaler/time2" default:"2020-04-14T21:26:00+02:00"` Invalid string } @@ -83,8 +69,8 @@ func TestProvider_Process(t *testing.T) { Value: aws.String("42.42"), }, { - Name: aws.String("/base/hex/hex1"), - Value: aws.String("737472696e6731"), + Name: aws.String("/base/text_unmarshaler/time1"), + Value: aws.String("2020-04-14T21:26:00+02:00"), }, }, }, @@ -115,8 +101,8 @@ func TestProvider_Process(t *testing.T) { "/base/float32/f322", "/base/float64/f641", "/base/float64/f642", - "/base/hex/hex1", - "/base/hex/hex2", + "/base/text_unmarshaler/time1", + "/base/text_unmarshaler/time2", } if !reflect.DeepEqual(names, expectedNames) { @@ -153,11 +139,12 @@ func TestProvider_Process(t *testing.T) { if s.F642 != 42.42 { t.Errorf("Process() F642 unexpected value: want %f, have %f", 42.42, s.F642) } - if s.H1.V != "string1" { - t.Errorf("Process() H1 unexpected value: want %s, have %s", "string1", s.H1.V) + tm, _ := time.Parse(time.RFC3339, "2020-04-14T21:26:00+02:00") + if !s.TU1.Equal(tm) { + t.Errorf("Process() TU1 unexpected value: want %v, have %v", tm, s.TU1) } - if s.H2.V != "string1" { - t.Errorf("Process() H2 unexpected value: want %s, have %s", "string1", s.H2.V) + if !s.TU2.Equal(tm) { + t.Errorf("Process() TU2 unexpected value: want %v, have %v", tm, s.TU2) } if s.Invalid != "" { t.Errorf("Process() Missing unexpected value: want %q, have %q", "", s.Invalid) @@ -208,6 +195,15 @@ func TestProvider_Process(t *testing.T) { client: &mockSSMClient{}, shouldErr: true, }, + { + name: "invalid unmarshal text", + configPath: "/base/", + c: &struct { + B1 bool `ssm:"/text_unmarshaler/time1" default:"notATime"` + }{}, + client: &mockSSMClient{}, + shouldErr: true, + }, { name: "missing required parameter", configPath: "/base/", From 94f54e515ddf0b75d2dcfc795cff209676560c6b Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Tue, 14 Apr 2020 22:38:58 +0200 Subject: [PATCH 03/13] rearrange code and add comment --- ssmconfig.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/ssmconfig.go b/ssmconfig.go index e2f3e71..6227a9f 100755 --- a/ssmconfig.go +++ b/ssmconfig.go @@ -34,6 +34,10 @@ type Provider struct { SSM ssmiface.SSMAPI } +const unmarshalTextMethod = "UnmarshalText" + +var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() + // Process loads config values from smm (parameter store) into c. Encrypted parameters // will automatically be decrypted. c must be a pointer to a struct. // @@ -139,24 +143,26 @@ func (p *Provider) getParameters(spec structSpec) (params map[string]string, inv } func isTextUnmarshaler(f reflect.StructField) bool { - tu := reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() - return reflect.PtrTo(f.Type).Implements(tu) + return reflect.PtrTo(f.Type).Implements(textUnmarshalerType) } +// Create a new instance of the field's type and call its UnmarshalText([]byte) method. +// Set the value after execution and fail if the method returns an error. func unmarshalText(f reflect.StructField, v reflect.Value, s string) error { newV := reflect.New(f.Type) + method := newV.MethodByName(unmarshalTextMethod) - m := newV.MethodByName("UnmarshalText") args := []reflect.Value{reflect.ValueOf([]byte(s))} - res := m.Call(args) + values := method.Call(args) + + v.Set(newV.Elem()) - if len(res) != 1 || !res[0].IsNil() { - err := res[0].Elem().Interface().(error) + // implementation only returns an error value + if !values[0].IsNil() { + err := values[0].Elem().Interface().(error) return errors.Errorf("could not decode %q into type %v: %v", s, f.Type.String(), err) } - v.Set(newV.Elem()) - return nil } From d6464b87ae0e98bb13c202c3dc2853d661fa5312 Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Tue, 14 Apr 2020 22:41:30 +0200 Subject: [PATCH 04/13] add one more comment --- ssmconfig.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ssmconfig.go b/ssmconfig.go index 6227a9f..dc8b445 100755 --- a/ssmconfig.go +++ b/ssmconfig.go @@ -142,6 +142,7 @@ func (p *Provider) getParameters(spec structSpec) (params map[string]string, inv return params, invalidParams, nil } +// Checks whether the value implements the TextUnmarshaler interface. func isTextUnmarshaler(f reflect.StructField) bool { return reflect.PtrTo(f.Type).Implements(textUnmarshalerType) } From bdbe44147ad5cf20bc068257d318169c73d46742 Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Tue, 14 Apr 2020 22:53:12 +0200 Subject: [PATCH 05/13] more appropriate names and subtitute type field --- ssmconfig.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ssmconfig.go b/ssmconfig.go index dc8b445..1390f29 100755 --- a/ssmconfig.go +++ b/ssmconfig.go @@ -90,16 +90,16 @@ func (p *Provider) Process(configPath string, c interface{}) error { continue } - field := v.Field(i) - structField := t.Field(i) + valueField := v.Field(i) + typeField := t.Field(i) var err error - if isTextUnmarshaler(structField) { - err = unmarshalText(structField, field, value) + if isTextUnmarshaler(typeField) { + err = unmarshalText(typeField, valueField, value) } else { - err = setValue(field, value) + err = setValue(valueField, value) } if err != nil { - return errors.Wrapf(err, "ssmconfig: error setting field %s", v.Type().Field(i).Name) + return errors.Wrapf(err, "ssmconfig: error setting field %s", typeField.Name) } } From e1209b77060bcd020fa4bbde3391e6ea0f725bf2 Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Tue, 14 Apr 2020 23:12:47 +0200 Subject: [PATCH 06/13] add example with time.Time to README.md --- README.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 3cbf01b..f7f458d 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,14 @@ AWS Lambda functions. It should be suitable for additional applications. Set some parameters in [AWS Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html): -| Name | Value | Type | Key ID | -| ---------------------------- | -------------------- | ------------ | ------------- | -| /exmaple_service/prod/debug | false | String | - | -| /exmaple_service/prod/port | 8080 | String | - | -| /exmaple_service/prod/user | Ian | String | - | -| /exmaple_service/prod/rate | 0.5 | String | - | -| /exmaple_service/prod/secret | zOcZkAGB6aEjN7SAoVBT | SecureString | alias/aws/ssm | +| Name | Value | Type | Key ID | +| ---------------------------- | -------------------- | ------------ | ------------- | +| /exmaple_service/prod/debug | false | String | - | +| /exmaple_service/prod/port | 8080 | String | - | +| /exmaple_service/prod/user | Ian | String | - | +| /exmaple_service/prod/rate | 0.5 | String | - | +| /exmaple_service/prod/secret | zOcZkAGB6aEjN7SAoVBT | SecureString | alias/aws/ssm | +| /exmaple_service/prod/ts | 2020-04-14T21:26:00+02:00 | String | - | Write some code: @@ -36,11 +37,12 @@ import ( ) type Config struct { - Debug bool `smm:"debug" default:"true"` - Port int `smm:"port"` - User string `smm:"user"` - Rate float32 `smm:"rate"` - Secret string `smm:"secret" required:"true"` + Debug bool `smm:"debug" default:"true"` + Port int `smm:"port"` + User string `smm:"user"` + Rate float32 `smm:"rate"` + Secret string `smm:"secret" required:"true"` + Timestamp time.Time `smm:"ts" required:"true"` } func main() { @@ -50,8 +52,8 @@ func main() { log.Fatal(err.Error()) } - format := "Debug: %v\nPort: %d\nUser: %s\nRate: %f\nSecret: %s\n" - _, err = fmt.Printf(format, c.Debug, c.Port, c.User, c.Rate, c.Secret) + format := "Debug: %v\nPort: %d\nUser: %s\nRate: %f\nSecret: %s\nTimestamp: %s\n" + _, err = fmt.Printf(format, c.Debug, c.Port, c.User, c.Rate, c.Secret, c.Timestamp) if err != nil { log.Fatal(err.Error()) } @@ -66,6 +68,7 @@ Port: 8080 User: Ian Rate: 0.500000 Secret: zOcZkAGB6aEjN7SAoVBT +Timestamp: 2020-04-14 21:26:00 +0200 +0200 ``` [Additional examples](https://godoc.org/github.com/ianlopshire/go-ssm-config#pkg-examples) can be found in godoc. @@ -101,6 +104,7 @@ ssmconfig supports these struct field types: * int, int8, int16, int32, int64 * bool * float32, float64 +* encoding.TextUnmarshaler More supported types may be added in the future. From a855d922bafbd497b5ca8c84caf816a09b6af4de Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Wed, 15 Apr 2020 22:35:52 +0200 Subject: [PATCH 07/13] fix wrong time1 error test and add net.IP as a second test --- ssmconfig_test.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ssmconfig_test.go b/ssmconfig_test.go index d8a998b..a127100 100755 --- a/ssmconfig_test.go +++ b/ssmconfig_test.go @@ -2,6 +2,7 @@ package ssmconfig_test import ( "errors" + "net" "reflect" "testing" "time" @@ -42,6 +43,8 @@ func TestProvider_Process(t *testing.T) { F642 float64 `ssm:"/float64/f642" default:"42.42"` TU1 time.Time `ssm:"/text_unmarshaler/time1"` TU2 time.Time `ssm:"/text_unmarshaler/time2" default:"2020-04-14T21:26:00+02:00"` + TU3 net.IP `ssm:"/text_unmarshaler/ipv41"` + TU4 net.IP `ssm:"/text_unmarshaler/ipv42" default:"127.0.0.1"` Invalid string } @@ -72,6 +75,10 @@ func TestProvider_Process(t *testing.T) { Name: aws.String("/base/text_unmarshaler/time1"), Value: aws.String("2020-04-14T21:26:00+02:00"), }, + { + Name: aws.String("/base/text_unmarshaler/ipv41"), + Value: aws.String("127.0.0.1"), + }, }, }, } @@ -103,6 +110,8 @@ func TestProvider_Process(t *testing.T) { "/base/float64/f642", "/base/text_unmarshaler/time1", "/base/text_unmarshaler/time2", + "/base/text_unmarshaler/ipv41", + "/base/text_unmarshaler/ipv42", } if !reflect.DeepEqual(names, expectedNames) { @@ -146,6 +155,13 @@ func TestProvider_Process(t *testing.T) { if !s.TU2.Equal(tm) { t.Errorf("Process() TU2 unexpected value: want %v, have %v", tm, s.TU2) } + ip := net.ParseIP("127.0.0.1") + if !s.TU3.Equal(ip) { + t.Errorf("Process() TU1 unexpected value: want %v, have %v", ip, s.TU3) + } + if !s.TU4.Equal(ip) { + t.Errorf("Process() TU2 unexpected value: want %v, have %v", ip, s.TU4) + } if s.Invalid != "" { t.Errorf("Process() Missing unexpected value: want %q, have %q", "", s.Invalid) } @@ -199,7 +215,16 @@ func TestProvider_Process(t *testing.T) { name: "invalid unmarshal text", configPath: "/base/", c: &struct { - B1 bool `ssm:"/text_unmarshaler/time1" default:"notATime"` + TU1 bool `ssm:"/text_unmarshaler/time1" default:"notATime"` + }{}, + client: &mockSSMClient{}, + shouldErr: true, + }, + { + name: "invalid unmarshal text", + configPath: "/base/", + c: &struct { + TU3 bool `ssm:"/text_unmarshaler/ipv41" default:"notAnIP"` }{}, client: &mockSSMClient{}, shouldErr: true, From e51fbdfe43f363e34db8ef3c65a25a0cb413cc1a Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Sat, 2 May 2020 21:17:06 +0200 Subject: [PATCH 08/13] move code around, simplify UnmarshalText invocation, fix error test --- ssmconfig.go | 36 ++++++++++++------------------------ ssmconfig_test.go | 4 ++-- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/ssmconfig.go b/ssmconfig.go index 1390f29..2144faf 100755 --- a/ssmconfig.go +++ b/ssmconfig.go @@ -91,15 +91,8 @@ func (p *Provider) Process(configPath string, c interface{}) error { } valueField := v.Field(i) - typeField := t.Field(i) - var err error - if isTextUnmarshaler(typeField) { - err = unmarshalText(typeField, valueField, value) - } else { - err = setValue(valueField, value) - } - if err != nil { - return errors.Wrapf(err, "ssmconfig: error setting field %s", typeField.Name) + if err = setValue(valueField, value); err != nil { + return errors.Wrapf(err, "ssmconfig: error setting field %s", v.Type().Name()) } } @@ -143,31 +136,26 @@ func (p *Provider) getParameters(spec structSpec) (params map[string]string, inv } // Checks whether the value implements the TextUnmarshaler interface. -func isTextUnmarshaler(f reflect.StructField) bool { - return reflect.PtrTo(f.Type).Implements(textUnmarshalerType) +func isTextUnmarshaler(t reflect.Type) bool { + return reflect.PtrTo(t).Implements(textUnmarshalerType) } // Create a new instance of the field's type and call its UnmarshalText([]byte) method. // Set the value after execution and fail if the method returns an error. -func unmarshalText(f reflect.StructField, v reflect.Value, s string) error { - newV := reflect.New(f.Type) - method := newV.MethodByName(unmarshalTextMethod) - - args := []reflect.Value{reflect.ValueOf([]byte(s))} - values := method.Call(args) - - v.Set(newV.Elem()) - - // implementation only returns an error value - if !values[0].IsNil() { - err := values[0].Elem().Interface().(error) - return errors.Errorf("could not decode %q into type %v: %v", s, f.Type.String(), err) +func unmarshalText(t reflect.Type, v reflect.Value, s string) error { + err := v.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(s)) + if err != nil { + return errors.Errorf("could not decode %q into type %v: %v", s, t.String(), err) } return nil } func setValue(v reflect.Value, s string) error { + t := v.Type() + if isTextUnmarshaler(t) { + return unmarshalText(t, v.Addr(), s) + } switch v.Kind() { case reflect.String: v.SetString(s) diff --git a/ssmconfig_test.go b/ssmconfig_test.go index a127100..1646c05 100755 --- a/ssmconfig_test.go +++ b/ssmconfig_test.go @@ -215,7 +215,7 @@ func TestProvider_Process(t *testing.T) { name: "invalid unmarshal text", configPath: "/base/", c: &struct { - TU1 bool `ssm:"/text_unmarshaler/time1" default:"notATime"` + TU1 time.Time `ssm:"/text_unmarshaler/time1" default:"notATime"` }{}, client: &mockSSMClient{}, shouldErr: true, @@ -224,7 +224,7 @@ func TestProvider_Process(t *testing.T) { name: "invalid unmarshal text", configPath: "/base/", c: &struct { - TU3 bool `ssm:"/text_unmarshaler/ipv41" default:"notAnIP"` + TU3 net.IP `ssm:"/text_unmarshaler/ipv41" default:"notAnIP"` }{}, client: &mockSSMClient{}, shouldErr: true, From eb92c7194a34fd4cff8d6a21690417e7f5e032f4 Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Sat, 2 May 2020 21:24:40 +0200 Subject: [PATCH 09/13] minimize code diff --- ssmconfig.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ssmconfig.go b/ssmconfig.go index 2144faf..b21c6a0 100755 --- a/ssmconfig.go +++ b/ssmconfig.go @@ -64,8 +64,7 @@ func (p *Provider) Process(configPath string, c interface{}) error { return errors.New("ssmconfig: c must be a pointer to a struct") } - t := v.Type() - spec := buildStructSpec(configPath, t) + spec := buildStructSpec(configPath, v.Type()) params, invalidPrams, err := p.getParameters(spec) if err != nil { @@ -90,9 +89,9 @@ func (p *Provider) Process(configPath string, c interface{}) error { continue } - valueField := v.Field(i) - if err = setValue(valueField, value); err != nil { - return errors.Wrapf(err, "ssmconfig: error setting field %s", v.Type().Name()) + err = setValue(v.Field(i), value) + if err != nil { + return errors.Wrapf(err, "ssmconfig: error setting field %s", v.Type().Field(i).Name) } } From 486ae734629d4022771cef810f63b0f0b069363b Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Sat, 2 May 2020 21:25:54 +0200 Subject: [PATCH 10/13] remove unused constant --- ssmconfig.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/ssmconfig.go b/ssmconfig.go index b21c6a0..2e8b96c 100755 --- a/ssmconfig.go +++ b/ssmconfig.go @@ -34,8 +34,6 @@ type Provider struct { SSM ssmiface.SSMAPI } -const unmarshalTextMethod = "UnmarshalText" - var textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() // Process loads config values from smm (parameter store) into c. Encrypted parameters From 60011fb5e21ad210f03b4a7500a5ae33dc890938 Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Mon, 4 May 2020 21:07:35 +0200 Subject: [PATCH 11/13] use ip test as pointer and add tests for missing parameter values that aren't required --- ssmconfig_test.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/ssmconfig_test.go b/ssmconfig_test.go index 1646c05..10a59e1 100755 --- a/ssmconfig_test.go +++ b/ssmconfig_test.go @@ -43,8 +43,10 @@ func TestProvider_Process(t *testing.T) { F642 float64 `ssm:"/float64/f642" default:"42.42"` TU1 time.Time `ssm:"/text_unmarshaler/time1"` TU2 time.Time `ssm:"/text_unmarshaler/time2" default:"2020-04-14T21:26:00+02:00"` - TU3 net.IP `ssm:"/text_unmarshaler/ipv41"` - TU4 net.IP `ssm:"/text_unmarshaler/ipv42" default:"127.0.0.1"` + TU3 time.Time `ssm:"/text_unmarshaler/time3"` + TUP1 *net.IP `ssm:"/text_unmarshaler/ipv41"` + TUP2 *net.IP `ssm:"/text_unmarshaler/ipv42" default:"127.0.0.1"` + TUP3 *net.IP `ssm:"/text_unmarshaler/ipv43"` Invalid string } @@ -110,8 +112,10 @@ func TestProvider_Process(t *testing.T) { "/base/float64/f642", "/base/text_unmarshaler/time1", "/base/text_unmarshaler/time2", + "/base/text_unmarshaler/time3", "/base/text_unmarshaler/ipv41", "/base/text_unmarshaler/ipv42", + "/base/text_unmarshaler/ipv43", } if !reflect.DeepEqual(names, expectedNames) { @@ -155,12 +159,18 @@ func TestProvider_Process(t *testing.T) { if !s.TU2.Equal(tm) { t.Errorf("Process() TU2 unexpected value: want %v, have %v", tm, s.TU2) } + if !s.TU3.Equal(time.Time{}) { + t.Errorf("Process() TU3 unexpected value: want %v, have %v", time.Time{}, s.TU3) + } ip := net.ParseIP("127.0.0.1") - if !s.TU3.Equal(ip) { - t.Errorf("Process() TU1 unexpected value: want %v, have %v", ip, s.TU3) + if !s.TUP1.Equal(ip) { + t.Errorf("Process() TUP1 unexpected value: want %v, have %v", ip, s.TUP1) + } + if !s.TUP2.Equal(ip) { + t.Errorf("Process() TUP2 unexpected value: want %v, have %v", ip, s.TUP2) } - if !s.TU4.Equal(ip) { - t.Errorf("Process() TU2 unexpected value: want %v, have %v", ip, s.TU4) + if s.TUP3 != nil { + t.Errorf("Process() TUP3 unexpected value: want %v, have %v", nil, s.TUP3) } if s.Invalid != "" { t.Errorf("Process() Missing unexpected value: want %q, have %q", "", s.Invalid) @@ -224,7 +234,7 @@ func TestProvider_Process(t *testing.T) { name: "invalid unmarshal text", configPath: "/base/", c: &struct { - TU3 net.IP `ssm:"/text_unmarshaler/ipv41" default:"notAnIP"` + TUP1 net.IP `ssm:"/text_unmarshaler/ipv41" default:"notAnIP"` }{}, client: &mockSSMClient{}, shouldErr: true, From 37828bdc75fd304d6471756be2e20d8514b81cdf Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Mon, 4 May 2020 21:07:59 +0200 Subject: [PATCH 12/13] add pointer support for TextUnmarshaler --- ssmconfig.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ssmconfig.go b/ssmconfig.go index 2e8b96c..d40e80d 100755 --- a/ssmconfig.go +++ b/ssmconfig.go @@ -132,14 +132,12 @@ func (p *Provider) getParameters(spec structSpec) (params map[string]string, inv return params, invalidParams, nil } -// Checks whether the value implements the TextUnmarshaler interface. -func isTextUnmarshaler(t reflect.Type) bool { - return reflect.PtrTo(t).Implements(textUnmarshalerType) -} - // Create a new instance of the field's type and call its UnmarshalText([]byte) method. // Set the value after execution and fail if the method returns an error. func unmarshalText(t reflect.Type, v reflect.Value, s string) error { + if v.IsNil() { + v.Set(reflect.New(t.Elem())) + } err := v.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(s)) if err != nil { return errors.Errorf("could not decode %q into type %v: %v", s, t.String(), err) @@ -150,7 +148,9 @@ func unmarshalText(t reflect.Type, v reflect.Value, s string) error { func setValue(v reflect.Value, s string) error { t := v.Type() - if isTextUnmarshaler(t) { + if t.Implements(textUnmarshalerType) { + return unmarshalText(t, v, s) + } else if reflect.PtrTo(t).Implements(textUnmarshalerType) { return unmarshalText(t, v.Addr(), s) } switch v.Kind() { From 527e6736503a5d0bb6cefeb075663698886ef3f1 Mon Sep 17 00:00:00 2001 From: Jasper Boot Date: Mon, 4 May 2020 21:31:02 +0200 Subject: [PATCH 13/13] add pointer example to README --- README.md | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f7f458d..8375fac 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,12 @@ Set some parameters in [AWS Parameter Store](https://docs.aws.amazon.com/systems | Name | Value | Type | Key ID | | ---------------------------- | -------------------- | ------------ | ------------- | -| /exmaple_service/prod/debug | false | String | - | -| /exmaple_service/prod/port | 8080 | String | - | -| /exmaple_service/prod/user | Ian | String | - | -| /exmaple_service/prod/rate | 0.5 | String | - | -| /exmaple_service/prod/secret | zOcZkAGB6aEjN7SAoVBT | SecureString | alias/aws/ssm | -| /exmaple_service/prod/ts | 2020-04-14T21:26:00+02:00 | String | - | +| /example_service/prod/debug | false | String | - | +| /example_service/prod/port | 8080 | String | - | +| /example_service/prod/user | Ian | String | - | +| /example_service/prod/rate | 0.5 | String | - | +| /example_service/prod/secret | zOcZkAGB6aEjN7SAoVBT | SecureString | alias/aws/ssm | +| /example_service/prod/ts | 2020-04-14T21:26:00+02:00 | String | - | Write some code: @@ -37,12 +37,13 @@ import ( ) type Config struct { - Debug bool `smm:"debug" default:"true"` - Port int `smm:"port"` - User string `smm:"user"` - Rate float32 `smm:"rate"` - Secret string `smm:"secret" required:"true"` - Timestamp time.Time `smm:"ts" required:"true"` + Debug bool `smm:"debug" default:"true"` + Port int `smm:"port"` + User string `smm:"user"` + Rate float32 `smm:"rate"` + Secret string `smm:"secret" required:"true"` + Timestamp time.Time `smm:"ts"` + AltIp *net.IP `smm:"alt_ip"` } func main() { @@ -52,8 +53,8 @@ func main() { log.Fatal(err.Error()) } - format := "Debug: %v\nPort: %d\nUser: %s\nRate: %f\nSecret: %s\nTimestamp: %s\n" - _, err = fmt.Printf(format, c.Debug, c.Port, c.User, c.Rate, c.Secret, c.Timestamp) + format := "Debug: %v\nPort: %d\nUser: %s\nRate: %f\nSecret: %s\nTimestamp: %s\nAltIp: %v\n" + _, err = fmt.Printf(format, c.Debug, c.Port, c.User, c.Rate, c.Secret, c.Timestamp, c.AltIp) if err != nil { log.Fatal(err.Error()) } @@ -69,6 +70,7 @@ User: Ian Rate: 0.500000 Secret: zOcZkAGB6aEjN7SAoVBT Timestamp: 2020-04-14 21:26:00 +0200 +0200 +AltIp: ``` [Additional examples](https://godoc.org/github.com/ianlopshire/go-ssm-config#pkg-examples) can be found in godoc. @@ -106,7 +108,7 @@ ssmconfig supports these struct field types: * float32, float64 * encoding.TextUnmarshaler -More supported types may be added in the future. +More supported types may be added in the future. Field types that implement the `encoding.TextUnmarshaler` interface can be used directly or as a pointer. ## Licence