diff --git a/README.md b/README.md index 3cbf01b..8375fac 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 | +| ---------------------------- | -------------------- | ------------ | ------------- | +| /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: @@ -36,11 +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"` + 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() { @@ -50,8 +53,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\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()) } @@ -66,6 +69,8 @@ Port: 8080 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. @@ -101,8 +106,9 @@ 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. +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 diff --git a/ssmconfig.go b/ssmconfig.go index 20a5f8a..d40e80d 100755 --- a/ssmconfig.go +++ b/ssmconfig.go @@ -3,6 +3,7 @@ package ssmconfig import ( + "encoding" "path" "reflect" "strconv" @@ -33,6 +34,8 @@ type Provider struct { SSM ssmiface.SSMAPI } +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. // @@ -129,7 +132,27 @@ func (p *Provider) getParameters(spec structSpec) (params map[string]string, inv return params, invalidParams, nil } +// 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) + } + + return nil +} + func setValue(v reflect.Value, s string) error { + t := v.Type() + 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() { case reflect.String: v.SetString(s) diff --git a/ssmconfig_test.go b/ssmconfig_test.go index b09adba..10a59e1 100755 --- a/ssmconfig_test.go +++ b/ssmconfig_test.go @@ -2,8 +2,10 @@ package ssmconfig_test import ( "errors" + "net" "reflect" "testing" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ssm" @@ -29,16 +31,22 @@ 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"` + 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"` + 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 } @@ -65,6 +73,14 @@ func TestProvider_Process(t *testing.T) { Name: aws.String("/base/float64/f641"), Value: aws.String("42.42"), }, + { + 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"), + }, }, }, } @@ -94,6 +110,12 @@ func TestProvider_Process(t *testing.T) { "/base/float32/f322", "/base/float64/f641", "/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) { @@ -130,6 +152,26 @@ 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) } + 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.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.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.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) } @@ -179,6 +221,24 @@ func TestProvider_Process(t *testing.T) { client: &mockSSMClient{}, shouldErr: true, }, + { + name: "invalid unmarshal text", + configPath: "/base/", + c: &struct { + TU1 time.Time `ssm:"/text_unmarshaler/time1" default:"notATime"` + }{}, + client: &mockSSMClient{}, + shouldErr: true, + }, + { + name: "invalid unmarshal text", + configPath: "/base/", + c: &struct { + TUP1 net.IP `ssm:"/text_unmarshaler/ipv41" default:"notAnIP"` + }{}, + client: &mockSSMClient{}, + shouldErr: true, + }, { name: "missing required parameter", configPath: "/base/",