From 8a4cf2a63d6d7e3c4a21ce6927f65c0e82c4124c Mon Sep 17 00:00:00 2001 From: Taichi Sasaki Date: Wed, 13 Sep 2023 02:26:16 +0900 Subject: [PATCH] Add parameterstore provider (#227) --- README.md | 1 + examples/read-parameterstore/README.md | 2 +- examples/read-parameterstore/main.go | 67 ++++++ providers/parameterstore/go.mod | 32 +++ providers/parameterstore/go.sum | 56 +++++ providers/parameterstore/parameterstore.go | 145 ++++++++++++ .../parameterstore/parameterstore_test.go | 210 ++++++++++++++++++ 7 files changed, 512 insertions(+), 1 deletion(-) create mode 100644 examples/read-parameterstore/main.go create mode 100644 providers/parameterstore/go.mod create mode 100644 providers/parameterstore/go.sum create mode 100644 providers/parameterstore/parameterstore.go create mode 100644 providers/parameterstore/parameterstore_test.go diff --git a/README.md b/README.md index 71ae6de0..09c264bf 100644 --- a/README.md +++ b/README.md @@ -664,6 +664,7 @@ Install with `go get -u github.com/knadh/koanf/providers/$provider` | appconfig | `vault.AppConfig(appconfig.Config{})` | AWS AppConfig provider | | etcd | `etcd.Provider(etcd.Config{})` | CNCF etcd provider | | consul | `consul.Provider(consul.Config{})` | Hashicorp Consul provider | +| parameterstore | `parameterstore.Provider(parameterstore.Config{})` | AWS Systems Manager Parameter Store provider | ### Bundled Parsers diff --git a/examples/read-parameterstore/README.md b/examples/read-parameterstore/README.md index 2e5666b5..80926d73 100644 --- a/examples/read-parameterstore/README.md +++ b/examples/read-parameterstore/README.md @@ -1,4 +1,4 @@ # AWS Parameter Store Example ## Link -[example](https://github.com/defensestation/koanf/blob/main/examples/read-parameterstore/main.go) \ No newline at end of file +[another example](https://github.com/defensestation/koanf/blob/main/examples/read-parameterstore/main.go) diff --git a/examples/read-parameterstore/main.go b/examples/read-parameterstore/main.go new file mode 100644 index 00000000..0f4e9efa --- /dev/null +++ b/examples/read-parameterstore/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/aws/aws-sdk-go-v2/service/ssm/types" + "github.com/knadh/koanf/providers/parameterstore" + "github.com/knadh/koanf/v2" +) + +var k = koanf.New(".") + +func main() { + // The configuration values are read from the environment variables. + c, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + log.Fatal(err) + } + client := ssm.NewFromConfig(c) + + for k, v := range map[string]string{ + "parent1": "alice", + "parent2.child1": "bob", + "parent2.child2.grandchild1": "carol", + } { + if _, err := client.PutParameter(context.TODO(), &ssm.PutParameterInput{ + Name: aws.String(k), + Value: aws.String(v), + Type: types.ParameterTypeSecureString, + Overwrite: aws.Bool(true), + }); err != nil { + log.Fatal(err) + } + } + + // Get a parameter. + if err := k.Load(parameterstore.ProviderWithClient(parameterstore.Config[ssm.GetParameterInput]{ + Delim: ".", + Input: ssm.GetParameterInput{Name: aws.String("parent1"), WithDecryption: aws.Bool(true)}, + }, client), nil); err != nil { + log.Fatalf("error loading config: %v", err) + } + fmt.Println(k.Sprint()) + + // Get parameters. + if err := k.Load(parameterstore.ProviderWithClient(parameterstore.Config[ssm.GetParametersInput]{ + Delim: ".", + Input: ssm.GetParametersInput{Names: []string{"parent1", "parent2.child1"}, WithDecryption: aws.Bool(true)}, + }, client), nil); err != nil { + log.Fatalf("error loading config: %v", err) + } + fmt.Println(k.Sprint()) + + // Get parameters by path. + if err := k.Load(parameterstore.ProviderWithClient(parameterstore.Config[ssm.GetParametersByPathInput]{ + Delim: ".", + Input: ssm.GetParametersByPathInput{Path: aws.String("/"), WithDecryption: aws.Bool(true)}, + }, client), nil); err != nil { + log.Fatalf("error loading config: %v", err) + } + fmt.Println(k.Sprint()) +} diff --git a/providers/parameterstore/go.mod b/providers/parameterstore/go.mod new file mode 100644 index 00000000..1f5accf6 --- /dev/null +++ b/providers/parameterstore/go.mod @@ -0,0 +1,32 @@ +module github.com/knadh/koanf/providers/parameterstore + +go 1.18 + +require ( + github.com/aws/aws-sdk-go-v2 v1.21.0 + github.com/aws/aws-sdk-go-v2/config v1.18.37 + github.com/aws/aws-sdk-go-v2/service/ssm v1.37.5 + github.com/aws/smithy-go v1.14.2 + github.com/knadh/koanf/maps v0.1.1 + github.com/knadh/koanf/v2 v2.0.1 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/aws/aws-sdk-go-v2/credentials v1.13.35 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.13.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/providers/parameterstore/go.sum b/providers/parameterstore/go.sum new file mode 100644 index 00000000..7eca42c2 --- /dev/null +++ b/providers/parameterstore/go.sum @@ -0,0 +1,56 @@ +github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc= +github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M= +github.com/aws/aws-sdk-go-v2/config v1.18.37 h1:RNAfbPqw1CstCooHaTPhScz7z1PyocQj0UL+l95CgzI= +github.com/aws/aws-sdk-go-v2/config v1.18.37/go.mod h1:8AnEFxW9/XGKCbjYDCJy7iltVNyEI9Iu9qC21UzhhgQ= +github.com/aws/aws-sdk-go-v2/credentials v1.13.35 h1:QpsNitYJu0GgvMBLUIYu9H4yryA5kMksjeIVQfgXrt8= +github.com/aws/aws-sdk-go-v2/credentials v1.13.35/go.mod h1:o7rCaLtvK0hUggAGclf76mNGGkaG5a9KWlp+d9IpcV8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 h1:uDZJF1hu0EVT/4bogChk8DyjSF6fof6uL/0Y26Ma7Fg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11/go.mod h1:TEPP4tENqBGO99KwVpV9MlOX4NSrSLP8u3KRy2CDwA8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 h1:22dGT7PneFMx4+b3pz7lMTRyN8ZKH7M2cW4GP9yUS2g= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41/go.mod h1:CrObHAuPneJBlfEJ5T3szXOUkLEThaGfvnhTf33buas= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 h1:SijA0mgjV8E+8G45ltVHs0fvKpTj8xmZJ3VwhGKtUSI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35/go.mod h1:SJC1nEVVva1g3pHAIdCp7QsRIkMmLAgoDquQ9Rr8kYw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42 h1:GPUcE/Yq7Ur8YSUk6lVkoIMWnJNO0HT18GUzCWCgCI0= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.42/go.mod h1:rzfdUlfA+jdgLDmPKjd3Chq9V7LVLYo1Nz++Wb91aRo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 h1:CdzPW9kKitgIiLV1+MHobfR5Xg25iYnyzWZhyQuSlDI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o= +github.com/aws/aws-sdk-go-v2/service/ssm v1.37.5 h1:s9QR0F1W5+11lq04OJ/mihpRpA2VDFIHmu+ktgAbNfg= +github.com/aws/aws-sdk-go-v2/service/ssm v1.37.5/go.mod h1:JjBzoceyKkpQY3v1GPIdg6kHqUFHRJ7SDlwtwoH0Qh8= +github.com/aws/aws-sdk-go-v2/service/sso v1.13.5 h1:oCvTFSDi67AX0pOX3PuPdGFewvLRU2zzFSrTsgURNo0= +github.com/aws/aws-sdk-go-v2/service/sso v1.13.5/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5 h1:dnInJb4S0oy8aQuri1mV6ipLlnZPfnsDNB9BGO9PDNY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4= +github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 h1:CQBFElb0LS8RojMJlxRSo/HXipvTZW2S44Lt9Mk2aYQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.21.5/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU= +github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ= +github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/v2 v2.0.1 h1:1dYGITt1I23x8cfx8ZnldtezdyaZtfAuRtIFOiRzK7g= +github.com/knadh/koanf/v2 v2.0.1/go.mod h1:ZeiIlIDXTE7w1lMT6UVcNiRAS2/rCeLn/GdLNvY1Dus= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/providers/parameterstore/parameterstore.go b/providers/parameterstore/parameterstore.go new file mode 100644 index 00000000..04df5f04 --- /dev/null +++ b/providers/parameterstore/parameterstore.go @@ -0,0 +1,145 @@ +// Package parameterstore implements a koanf.Provider for AWS Systems Manager Parameter Store. +package parameterstore + +import ( + "context" + "errors" + + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/knadh/koanf/maps" +) + +// Input is a constraint that permits any input type +// to get paramers from AWS Systems Manager Parameter Store. +type Input interface { + ssm.GetParameterInput | ssm.GetParametersInput | ssm.GetParametersByPathInput +} + +// Config represents a ParameterStore provider configuration. +type Config[T Input] struct { + // Delim is the delimiter to use + // when specifying config key paths, for instance a . for `parent.child.key` + // or a / for `parent/child/key`. + Delim string + + // Input is the input to get parameters. + Input T + + // OptFns is the additional functional options to get parameters. + OptFns []func(*ssm.Options) + + // Callback is an optional callback that takes a (key, value) + // with the variable name and value and allows you to modify both. + // If the callback returns an empty key, the variable will be ignored. + Callback func(key, value string) (string, interface{}) +} + +// ParameterStore implements an AWS Systems Manager Parameter Store provider. +type ParameterStore[T Input] struct { + client *ssm.Client + config Config[T] +} + +// Provider returns a ParameterStore provider. +// The AWS Systems Manager Client is configured via environment variables. +// The configuration values are read from the environment variables. +// - AWS_REGION +// - AWS_ACCESS_KEY_ID +// - AWS_SECRET_ACCESS_KEY +// - AWS_SESSION_TOKEN +func Provider[T Input](config Config[T]) *ParameterStore[T] { + c, err := awsconfig.LoadDefaultConfig(context.TODO()) + if err != nil { + return nil + } + return ProviderWithClient[T](config, ssm.NewFromConfig(c)) +} + +// ProviderWithClient returns a ParameterStore provider +// using an existing AWS Systems Manager client. +func ProviderWithClient[T Input](config Config[T], client *ssm.Client) *ParameterStore[T] { + return &ParameterStore[T]{ + client: client, + config: config, + } +} + +// ReadBytes is not supported by the ParameterStore provider. +func (ps *ParameterStore[T]) ReadBytes() ([]byte, error) { + return nil, errors.New("parameterstore provider does not support this method") +} + +// Read returns a nested config map. +func (ps *ParameterStore[T]) Read() (map[string]interface{}, error) { + var ( + mp = make(map[string]interface{}) + ) + switch input := interface{}(ps.config.Input).(type) { + case ssm.GetParameterInput: + output, err := ps.client.GetParameter(context.TODO(), &input, ps.config.OptFns...) + if err != nil { + return nil, err + } + // If there's a transformation callback, run it. + if ps.config.Callback != nil { + name, value := ps.config.Callback(*output.Parameter.Name, *output.Parameter.Value) + // If the callback blanked the key, it should be omitted. + if name == "" { + break + } + mp[name] = value + } else { + mp[*output.Parameter.Name] = *output.Parameter.Value + } + case ssm.GetParametersInput: + output, err := ps.client.GetParameters(context.TODO(), &input, ps.config.OptFns...) + if err != nil { + return nil, err + } + for _, p := range output.Parameters { + // If there's a transformation callback, run it. + if ps.config.Callback != nil { + name, value := ps.config.Callback(*p.Name, *p.Value) + // If the callback blanked the key, it should be omitted. + if name == "" { + break + } + mp[name] = value + } else { + mp[*p.Name] = *p.Value + } + } + case ssm.GetParametersByPathInput: + var nextToken *string + for { + input.NextToken = nextToken + output, err := ps.client.GetParametersByPath(context.TODO(), &input, ps.config.OptFns...) + if err != nil { + return nil, err + } + for _, p := range output.Parameters { + // If there's a transformation callback, run it. + if ps.config.Callback != nil { + name, value := ps.config.Callback(*p.Name, *p.Value) + // If the callback blanked the key, it should be omitted. + if name == "" { + break + } + mp[name] = value + } else { + mp[*p.Name] = *p.Value + } + } + if output.NextToken == nil { + break + } + nextToken = output.NextToken + } + } + // Unflatten only when a delimiter is specified. + if ps.config.Delim != "" { + mp = maps.Unflatten(mp, ps.config.Delim) + } + return mp, nil +} diff --git a/providers/parameterstore/parameterstore_test.go b/providers/parameterstore/parameterstore_test.go new file mode 100644 index 00000000..7611b6d2 --- /dev/null +++ b/providers/parameterstore/parameterstore_test.go @@ -0,0 +1,210 @@ +package parameterstore + +import ( + "context" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/ssm" + "github.com/aws/aws-sdk-go-v2/service/ssm/types" + "github.com/aws/smithy-go/middleware" + "github.com/knadh/koanf/v2" + "github.com/stretchr/testify/assert" +) + +func TestParameterStore(t *testing.T) { + c, err := config.LoadDefaultConfig( + context.TODO(), + config.WithRegion("us-east-1"), + config.WithAPIOptions([]func(*middleware.Stack) error{ + // Mock the SDK response using the middleware. + func(stack *middleware.Stack) error { + type key struct{} + err := stack.Initialize.Add( + middleware.InitializeMiddlewareFunc( + "MockInitialize", + func(ctx context.Context, in middleware.InitializeInput, next middleware.InitializeHandler) (out middleware.InitializeOutput, metadata middleware.Metadata, err error) { + switch v := in.Parameters.(type) { + case *ssm.GetParametersByPathInput: + ctx = middleware.WithStackValue(ctx, key{}, v.NextToken) + } + return next.HandleInitialize(ctx, in) + }, + ), middleware.Before, + ) + if err != nil { + return err + } + return stack.Finalize.Add( + middleware.FinalizeMiddlewareFunc( + "MockFinalize", + func(ctx context.Context, input middleware.FinalizeInput, handler middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { + switch awsmiddleware.GetOperationName(ctx) { + case "GetParameter": + return middleware.FinalizeOutput{ + Result: &ssm.GetParameterOutput{ + Parameter: &types.Parameter{ + Name: aws.String("prefix.parent1"), + Value: aws.String("alice"), + }, + }, + }, middleware.Metadata{}, nil + case "GetParameters": + return middleware.FinalizeOutput{ + Result: &ssm.GetParametersOutput{ + Parameters: []types.Parameter{ + { + Name: aws.String("prefix.parent1"), + Value: aws.String("alice"), + }, + { + Name: aws.String("prefix.parent2.child1"), + Value: aws.String("bob"), + }, + }, + }, + }, middleware.Metadata{}, nil + case "GetParametersByPath": + var output ssm.GetParametersByPathOutput + if middleware.GetStackValue(ctx, key{}) == (*string)(nil) { + output = ssm.GetParametersByPathOutput{ + NextToken: aws.String("nextToken"), + Parameters: []types.Parameter{ + { + Name: aws.String("prefix.parent1"), + Value: aws.String("alice"), + }, + { + Name: aws.String("prefix.parent2.child1"), + Value: aws.String("bob"), + }, + }, + } + } else { + output = ssm.GetParametersByPathOutput{ + Parameters: []types.Parameter{ + { + Name: aws.String("prefix.parent2.child2.grandchild1"), + Value: aws.String("carol"), + }, + }, + } + } + return middleware.FinalizeOutput{Result: &output}, middleware.Metadata{}, nil + default: + return middleware.FinalizeOutput{}, middleware.Metadata{}, nil + } + }, + ), + middleware.Before, + ) + }, + }), + ) + assert.NoError(t, err) + client := ssm.NewFromConfig(c) + + tests := map[string]struct { + provider koanf.Provider + want map[string]interface{} + }{ + "get a parameter": { + provider: ProviderWithClient(Config[ssm.GetParameterInput]{ + Delim: ".", + Input: ssm.GetParameterInput{Name: aws.String("parent1")}, + Callback: func(key, value string) (string, interface{}) { return strings.TrimPrefix(key, "prefix."), value }, + }, client), + want: map[string]interface{}{ + "parent1": "alice", + }, + }, + "get parameters": { + provider: ProviderWithClient(Config[ssm.GetParametersInput]{ + Delim: ".", + Input: ssm.GetParametersInput{Names: []string{"parent1", "parent2.child1"}}, + Callback: func(key, value string) (string, interface{}) { return strings.TrimPrefix(key, "prefix."), value }, + }, client), + want: map[string]interface{}{ + "parent1": "alice", + "parent2": map[string]interface{}{ + "child1": "bob", + }, + }, + }, + "get parameters by path": { + provider: ProviderWithClient(Config[ssm.GetParametersByPathInput]{ + Delim: ".", + Input: ssm.GetParametersByPathInput{Path: aws.String("/")}, + Callback: func(key, value string) (string, interface{}) { return strings.TrimPrefix(key, "prefix."), value }, + }, client), + want: map[string]interface{}{ + "parent1": "alice", + "parent2": map[string]interface{}{ + "child1": "bob", + "child2": map[string]interface{}{ + "grandchild1": "carol", + }, + }, + }, + }, + "get a parameter but it is ignored": { + provider: ProviderWithClient(Config[ssm.GetParameterInput]{ + Delim: ".", + Input: ssm.GetParameterInput{Name: aws.String("parent1")}, + Callback: func(key, value string) (string, interface{}) { + return strings.TrimPrefix(strings.TrimPrefix(key, "prefix."), "parent1"), value + }, + }, client), + want: map[string]interface{}{ + // Ignored. + // "parent1": "alice", + }, + }, + "get parameters but one is ignored": { + provider: ProviderWithClient(Config[ssm.GetParametersInput]{ + Delim: ".", + Input: ssm.GetParametersInput{Names: []string{"parent1", "parent2.child1"}}, + Callback: func(key, value string) (string, interface{}) { + return strings.TrimPrefix(strings.TrimPrefix(key, "prefix."), "parent2.child1"), value + }, + }, client), + want: map[string]interface{}{ + "parent1": "alice", + // Ignored. + // "parent2": map[string]interface{}{ + // "child1": "bob", + // }, + }, + }, + "get parameters by path but ont is ignored": { + provider: ProviderWithClient(Config[ssm.GetParametersByPathInput]{ + Delim: ".", + Input: ssm.GetParametersByPathInput{Path: aws.String("/")}, + Callback: func(key, value string) (string, interface{}) { + return strings.TrimPrefix(strings.TrimPrefix(key, "prefix."), "parent2.child2.grandchild1"), value + }, + }, client), + want: map[string]interface{}{ + "parent1": "alice", + "parent2": map[string]interface{}{ + "child1": "bob", + // Ignored. + // "child2": map[string]interface{}{ + // "grandchild1": "carol", + // }, + }, + }, + }, + } + for name, test := range tests { + test := test + t.Run(name, func(t *testing.T) { + got, err := test.provider.Read() + assert.NoError(t, err) + assert.Equal(t, test.want, got) + }) + } +}