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

[Feature] Add nested struct/*struct and embedded struct #8

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/go-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jobs:
runs-on: ubuntu-16.04
strategy:
matrix:
go: ['1.10', '1.11', '1.12', '1.13' ]
go: ['1.11', '1.12', '1.13', '1.14', '1.15']
steps:
- uses: actions/checkout@master
- name: Setup go
Expand Down
38 changes: 25 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package is largely inspired by [kelseyhightower/envconfig](https://github.com/ke

## Motivation

This package was created to reduce the boilerplate code required when using Parameter Store to provide configuration to
This package was created to reduce the boilerplate code required when using Parameter Store to provide configuration to
AWS Lambda functions. It should be suitable for additional applications.

## Usage
Expand All @@ -21,7 +21,7 @@ Set some parameters in [AWS Parameter Store](https://docs.aws.amazon.com/systems
| /exmaple_service/prod/user | Ian | String | - |
| /exmaple_service/prod/rate | 0.5 | String | - |
| /exmaple_service/prod/secret | zOcZkAGB6aEjN7SAoVBT | SecureString | alias/aws/ssm |

Write some code:

```go
Expand All @@ -34,13 +34,24 @@ import (

ssmconfig "github.com/ianlopshire/go-ssm-config"
)
type DbConfig struct {
Host string
Name string
Pass string `ssm:"/db/password" required:"true"`
}

type LogConfig struct {
LogSamplingRate int `ssm:"log_sample_rate"`
}

type Config struct {
Debug bool `ssm:"debug" default:"true"`
Port int `ssm:"port"`
User string `ssm:"user"`
Rate float32 `ssm:"rate"`
Secret string `ssm:"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"`
DB DbConfig
LogSamplingRate
}

func main() {
Expand All @@ -49,7 +60,7 @@ func main() {
if err != nil {
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)
if err != nil {
Expand Down Expand Up @@ -97,13 +108,14 @@ The behavior of using the `default` and `required` tags on the same struct field

ssmconfig supports these struct field types:

* string
* int, int8, int16, int32, int64
* bool
* float32, float64
- string
- int, int8, int16, int32, int64
- bool
- float32, float64
- struct, \*struct

More supported types may be added in the future.

## Licence

MIT
MIT
119 changes: 74 additions & 45 deletions ssmconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ func Process(configPath string, c interface{}) error {
return p.Process(configPath, c)
}

// Provider is a ssm configuration provider.
type Provider struct {
SSM ssmiface.SSMAPI
}
Expand All @@ -48,59 +47,37 @@ type Provider struct {
// The behavior of using the `default` and `required` tags on the same struct field is
// currently undefined.
func (p *Provider) Process(configPath string, c interface{}) error {

v := reflect.ValueOf(c)
if v.Kind() != reflect.Ptr || v.IsNil() {
return errors.New("ssmconfig: c must be a pointer to a struct")
}

v = reflect.Indirect(reflect.ValueOf(c))
if v.Kind() != reflect.Struct {
return errors.New("ssmconfig: c must be a pointer to a struct")
}
// get params required from struct
spec := make(map[string]fieldSpec)
buildStructSpec(configPath, v.Type(), spec)

spec := buildStructSpec(configPath, v.Type())

// get params from ssm parameter store
params, invalidPrams, err := p.getParameters(spec)
if err != nil {
return errors.Wrap(err, "ssmconfig: could not get parameters")
}

for i, field := range spec {
if field.name == "" && field.defaultValue == "" {
continue
}

if _, ok := invalidPrams[field.name]; ok && field.required {
return errors.Errorf("ssmconfig: %s is required", field.name)
}

value, ok := params[field.name]
if !ok {
value = field.defaultValue
}

if value == "" {
continue
}

err = setValue(v.Field(i), value)
if err != nil {
return errors.Wrapf(err, "ssmconfig: error setting field %s", v.Type().Field(i).Name)
}
}

return nil
// set values in struct
return setValues(v, params, invalidPrams, spec)
}

func (p *Provider) getParameters(spec structSpec) (params map[string]string, invalidParams map[string]struct{}, err error) {
func (p *Provider) getParameters(spec map[string]fieldSpec) (params map[string]string, invalidParams map[string]struct{}, err error) {
// find all of the params that need to be requested
var names []*string
for i := range spec {
if spec[i].name == "" {
for key, val := range spec {
if val.name == "" {
continue
}
names = append(names, &spec[i].name)
curr := spec[key]
names = append(names, &curr.name)
}

input := &ssm.GetParametersInput{
Expand Down Expand Up @@ -129,7 +106,53 @@ func (p *Provider) getParameters(spec structSpec) (params map[string]string, inv
return params, invalidParams, nil
}

func setValue(v reflect.Value, s string) error {
func setValues(v reflect.Value, params map[string]string, invalidParams map[string]struct{}, spec map[string]fieldSpec) error {
if v.Kind() == reflect.Ptr {
// Return on nil pointer
if v.IsNil() {
return nil
}
v = reflect.Indirect(v)
}

for i := 0; i < v.NumField(); i++ {
// Add support for struct pointers
ft := v.Type().Field(i).Type
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}

if ft.Kind() == reflect.Struct {
if err := setValues(v.Field(i), params, invalidParams, spec); err != nil {
return err
}
continue
}
name := v.Type().Field(i).Tag.Get("ssm")
if name == "" {
continue
}
field := spec[name]
if _, ok := invalidParams[field.name]; ok && field.required {
return errors.Errorf("ssmconfig: %s is required", field.name)
}

value, ok := params[field.name]
if !ok {
value = field.defaultValue
}
if value == "" {
continue
}
err := setBasicValue(v.Field(i), value)
if err != nil {
return errors.Wrapf(err, "ssmconfig: error setting field %s", v.Type().Field(i).Name)
}
}
return nil
}

func setBasicValue(v reflect.Value, s string) error {
switch v.Kind() {
case reflect.String:
v.SetString(s)
Expand Down Expand Up @@ -168,26 +191,32 @@ func setValue(v reflect.Value, s string) error {
return nil
}

type structSpec []fieldSpec

type fieldSpec struct {
name string
defaultValue string
required bool
}

func buildStructSpec(configPath string, t reflect.Type) (spec structSpec) {
func buildStructSpec(configPath string, t reflect.Type, spec map[string]fieldSpec) {
for i := 0; i < t.NumField(); i++ {
name := t.Field(i).Tag.Get("ssm")
if name != "" {
name = path.Join(configPath, name)
// Add support for struct pointers
ft := t.Field(i).Type
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}

spec = append(spec, fieldSpec{
name: name,
if ft.Kind() == reflect.Struct {
buildStructSpec(configPath, ft, spec)
continue
}
name := t.Field(i).Tag.Get("ssm")
if name == "" {
continue
}
spec[name] = fieldSpec{
name: path.Join(configPath, name),
defaultValue: t.Field(i).Tag.Get("default"),
required: t.Field(i).Tag.Get("required") == "true",
})
}
}
return spec
}
51 changes: 48 additions & 3 deletions ssmconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ func (c *mockSSMClient) GetParameters(input *ssm.GetParametersInput) (*ssm.GetPa

func TestProvider_Process(t *testing.T) {
t.Run("base case", func(t *testing.T) {
type d struct {
D1 string `ssm:"/strings/d1"`
D2 struct {
D3 string `ssm:"/strings/d2"`
}
}
type e struct {
E1 string `ssm:"/strings/e1"`
}
var s struct {
S1 string `ssm:"/strings/s1"`
S2 string `ssm:"/strings/s2" default:"string2"`
Expand All @@ -40,6 +49,9 @@ func TestProvider_Process(t *testing.T) {
F641 float64 `ssm:"/float64/f641"`
F642 float64 `ssm:"/float64/f642" default:"42.42"`
Invalid string
D d
DPntr *d
e
}

mc := &mockSSMClient{
Expand All @@ -65,6 +77,18 @@ func TestProvider_Process(t *testing.T) {
Name: aws.String("/base/float64/f641"),
Value: aws.String("42.42"),
},
{
Name: aws.String("/base/strings/d1"),
Value: aws.String("string3"),
},
{
Name: aws.String("/base/strings/d2"),
Value: aws.String("string4"),
},
{
Name: aws.String("/base/strings/e1"),
Value: aws.String("string5"),
},
},
},
}
Expand All @@ -73,6 +97,9 @@ func TestProvider_Process(t *testing.T) {
SSM: mc,
}

// Add Pointer Struct
s.DPntr = &d{}

err := p.Process("/base/", &s)

if err != nil {
Expand All @@ -85,18 +112,21 @@ func TestProvider_Process(t *testing.T) {
}
expectedNames := []string{
"/base/strings/s1",
"/base/bool/b1",
"/base/float64/f642",
"/base/strings/s2",
"/base/int/i1",
"/base/int/i2",
"/base/bool/b1",
"/base/bool/b2",
"/base/float32/f321",
"/base/float32/f322",
"/base/float64/f641",
"/base/float64/f642",
"/base/string/d1",
"/base/string/d2",
"/base/string/e1",
}

if !reflect.DeepEqual(names, expectedNames) {
if len(names) != len(expectedNames) {
t.Errorf("Process() unexpected input names: have %v, want %v", names, expectedNames)
}

Expand Down Expand Up @@ -133,6 +163,21 @@ func TestProvider_Process(t *testing.T) {
if s.Invalid != "" {
t.Errorf("Process() Missing unexpected value: want %q, have %q", "", s.Invalid)
}
if s.D.D1 != "string3" {
t.Errorf("Process() D1 unexpected value: want %s, have %s", "string3", s.D.D1)
}
if s.D.D2.D3 != "string4" {
t.Errorf("Process() D2 unexpected value: want %s, have %s", "string4", s.D.D2.D3)
}
if s.DPntr.D1 != "string3" {
t.Errorf("Process() D1 unexpected value: want %s, have %s", "string3", s.DPntr.D1)
}
if s.DPntr.D2.D3 != "string4" {
t.Errorf("Process() D2 unexpected value: want %s, have %s", "string4", s.DPntr.D2.D3)
}
if s.E1 != "string5" {
t.Errorf("Process() D2 unexpected value: want %s, have %s", "string5", s.E1)
}
})

for _, tt := range []struct {
Expand Down