Skip to content

Commit

Permalink
implement ssm backend (#71)
Browse files Browse the repository at this point in the history
This implements a backend that uses the Amazon AWS SSM parameter store for keys.
  • Loading branch information
steamrolla authored and rogpeppe committed Sep 26, 2019
1 parent 1982253 commit 1833a32
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Confita is a library that loads configuration from multiple backends and stores
- [etcd](https://github.com/coreos/etcd)
- [Consul](https://www.consul.io/)
- [Vault](https://www.vaultproject.io/)
- [Amazon SSM](https://docs.aws.amazon.com/systems-manager/latest/userguide/what-is-systems-manager.html)

## Install

Expand Down
88 changes: 88 additions & 0 deletions backend/ssm/ssm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package ssm

import (
"context"
"strings"

"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
"github.com/heetch/confita/backend"
)

type Backend struct {
client ssmiface.SSMAPI
ssmPath string
cache map[string][]byte
}

func NewBackend(ssm ssmiface.SSMAPI, path string) *Backend {
return &Backend{client: ssm, ssmPath: path}
}

func (b *Backend) Get(ctx context.Context, key string) ([]byte, error) {
if b.cache == nil {
err := b.fetchParams(ctx)
if err != nil {
return nil, err
}
}

return b.fromCache(ctx, key)
}

func (b *Backend) Name() string {
return "ssm"
}

func (b *Backend) fetchParams(ctx context.Context) error {
b.cache = make(map[string][]byte)

ssmInput := &ssm.GetParametersByPathInput{
Path: &b.ssmPath,
Recursive: newBool(true),
WithDecryption: newBool(true),
MaxResults: newInt64(10),
}

for {
res, err := b.client.GetParametersByPathWithContext(ctx, ssmInput)
if err != nil {
return err
}

for _, p := range res.Parameters {
if p.Name != nil && p.Value != nil {
path := strings.Split(*p.Name, "/")
key := path[len(path)-1]
if key != "" {
b.cache[key] = []byte(*p.Value)
}
}
}

if res.NextToken == nil {
break
}

ssmInput.NextToken = res.NextToken
}

return nil
}

func (b *Backend) fromCache(ctx context.Context, key string) ([]byte, error) {
v, ok := b.cache[key]
if !ok {
return nil, backend.ErrNotFound
}

return v, nil
}

func newBool(b bool) *bool {
return &b
}

func newInt64(i int64) *int64 {
return &i
}
181 changes: 181 additions & 0 deletions backend/ssm/ssm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package ssm

import (
"context"
"fmt"
"testing"

"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/aws-sdk-go/service/ssm/ssmiface"
"github.com/heetch/confita/backend"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

type mockSSM struct {
mock.Mock
ssmiface.SSMAPI
}

func (_m *mockSSM) GetParametersByPathWithContext(_a0 context.Context, _a1 *ssm.GetParametersByPathInput, _a2 ...request.Option) (*ssm.GetParametersByPathOutput, error) {
_va := make([]interface{}, len(_a2))
for _i := range _a2 {
_va[_i] = _a2[_i]
}
var _ca []interface{}
_ca = append(_ca, _a0, _a1)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)

var r0 *ssm.GetParametersByPathOutput
if rf, ok := ret.Get(0).(func(context.Context, *ssm.GetParametersByPathInput, ...request.Option) *ssm.GetParametersByPathOutput); ok {
r0 = rf(_a0, _a1, _a2...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*ssm.GetParametersByPathOutput)
}
}

var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *ssm.GetParametersByPathInput, ...request.Option) error); ok {
r1 = rf(_a0, _a1, _a2...)
} else {
r1 = ret.Error(1)
}

return r0, r1
}

func TestAWSError(t *testing.T) {
client := new(mockSSM)
ssmOpts := getSSMOpts("/borked/")
ctx := context.Background()
expected := fmt.Errorf("aws down")
client.On("GetParametersByPathWithContext", ctx, ssmOpts).Return(
nil, expected)

b := NewBackend(client, "/borked/")
_, actual := b.Get(context.Background(), "some_key")
require.Equal(t, expected, actual)
}

func TestNilNameAndValue(t *testing.T) {
client := new(mockSSM)
ssmOpts := getSSMOpts("/sup/")
ctx := context.Background()

client.On("GetParametersByPathWithContext", ctx, ssmOpts).Return(&ssm.GetParametersByPathOutput{
Parameters: []*ssm.Parameter{
{
Name: nil,
Value: nil,
},
{
Name: ptrString("/sup/key"),
Value: nil,
},
},
}, nil)

b := NewBackend(client, "/sup/")

_, actual := b.Get(context.Background(), "key")
require.Equal(t, backend.ErrNotFound, actual)
}

func TestEmptyKey(t *testing.T) {
client := new(mockSSM)
ssmOpts := getSSMOpts("/sup/")
ctx := context.Background()

client.On("GetParametersByPathWithContext", ctx, ssmOpts).Return(&ssm.GetParametersByPathOutput{
Parameters: []*ssm.Parameter{
{
Name: ptrString("/sup/"),
Value: ptrString("a value"),
},
},
}, nil)

b := NewBackend(client, "/sup/")

_, actual := b.Get(context.Background(), "")
require.Equal(t, backend.ErrNotFound, actual)
}

func TestKeyNotFound(t *testing.T) {
client := new(mockSSM)
ssmOpts := getSSMOpts("/whatevs/")
ctx := context.Background()
client.On("GetParametersByPathWithContext", ctx, ssmOpts).Return(
&ssm.GetParametersByPathOutput{}, nil)

b := NewBackend(client, "/whatevs/")
_, actual := b.Get(context.Background(), "some_key")
require.Equal(t, backend.ErrNotFound, actual)
}

func ptrString(str string) *string {
return &str
}

func TestKeysFound(t *testing.T) {
client := new(mockSSM)
ctx := context.Background()
ssmOpts := getSSMOpts("/yo/whatup/")
client.On("GetParametersByPathWithContext", ctx, ssmOpts).Return(
&ssm.GetParametersByPathOutput{
Parameters: []*ssm.Parameter{
{Name: ptrString("/yo/whatup/a_key"), Value: ptrString("wow")},
{Name: ptrString("/yo/whatup/some_key"), Value: ptrString("wondrous")},
},
}, nil)

b := NewBackend(client, "/yo/whatup/")
actual, err := b.Get(context.Background(), "a_key")
require.Nil(t, err)
require.Equal(t, "wow", string(actual))
actual, err = b.Get(context.Background(), "some_key")
require.Nil(t, err)
require.Equal(t, "wondrous", string(actual))
}

func TestSSMPagedCall(t *testing.T) {
client := new(mockSSM)
ctx := context.Background()
firstOpts := getSSMOpts("/a/path/")
client.On("GetParametersByPathWithContext", ctx, firstOpts).Return(
&ssm.GetParametersByPathOutput{
Parameters: []*ssm.Parameter{},
NextToken: ptrString("/a/path/your_key"),
}, nil)

secondOpts := getSSMOpts("/a/path/")
secondOpts.NextToken = ptrString("/a/path/your_key")
client.On("GetParametersByPathWithContext", ctx, secondOpts).Return(
&ssm.GetParametersByPathOutput{
Parameters: []*ssm.Parameter{
{Name: ptrString("/a/path/your_key"), Value: ptrString("shazam")},
{Name: ptrString("/a/path/another_key"), Value: ptrString("kazam")},
},
NextToken: nil,
}, nil)

b := NewBackend(client, "/a/path/")
actual, err := b.Get(context.Background(), "your_key")
require.Nil(t, err)
require.Equal(t, "shazam", string(actual))
actual, err = b.Get(context.Background(), "another_key")
require.Nil(t, err)
require.Equal(t, "kazam", string(actual))
}

func getSSMOpts(path string) *ssm.GetParametersByPathInput {
return &ssm.GetParametersByPathInput{
Path: &path,
Recursive: newBool(true),
WithDecryption: newBool(true),
MaxResults: newInt64(10),
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
github.com/aws/aws-sdk-go v1.23.20
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cenkalti/backoff v2.1.1+incompatible // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/aws/aws-sdk-go v1.23.20 h1:2CBuL21P0yKdZN5urf2NxKa1ha8fhnY+A3pBCHFeZoA=
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
Expand Down Expand Up @@ -182,6 +184,8 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jefferai/jsonx v1.0.0 h1:Xoz0ZbmkpBvED5W9W1B5B/zc3Oiq7oXqiW7iRV3B6EI=
github.com/jefferai/jsonx v1.0.0/go.mod h1:OGmqmi2tTeI/PS+qQfBDToLHHJIy/RMp24fPo8vFvoQ=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
Expand Down Expand Up @@ -288,6 +292,7 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
Expand Down

0 comments on commit 1833a32

Please sign in to comment.