Skip to content
This repository has been archived by the owner on Aug 22, 2019. It is now read-only.

Commit

Permalink
AWS support (#1)
Browse files Browse the repository at this point in the history
* add AWS support

* tests and such
  • Loading branch information
tizz98 authored Jun 15, 2019
1 parent 89de90b commit 4490d12
Show file tree
Hide file tree
Showing 11 changed files with 368 additions and 37 deletions.
15 changes: 15 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Golang CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-go/ for more details
version: 2
jobs:
build:
docker:
# specify the version
- image: circleci/golang:1.12
working_directory: /go/src/github.com/hookactions/config
environment: # environment variables for the build itself
GO111MODULE: 'on'
steps:
- checkout
- run: make test
8 changes: 0 additions & 8 deletions .github/FUNDING.yml

This file was deleted.

14 changes: 0 additions & 14 deletions .travis.yml

This file was deleted.

12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
test:
go get golang.org/x/tools/cmd/goimports
go get -u golang.org/x/lint/golint

go vet ./...

golint -set_exit_status
test -z "$(goimports -l .)"

go test -v ./... -race -cover

go mod tidy
40 changes: 27 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
# Config
[![Documentation](https://godoc.org/github.com/JeremyLoy/config?status.svg)](http://godoc.org/github.com/JeremyLoy/config)
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go)
[![Build Status](https://travis-ci.org/JeremyLoy/config.svg?branch=master)](https://travis-ci.org/JeremyLoy/config)
[![Go Report Card](https://goreportcard.com/badge/github.com/JeremyLoy/config)](https://goreportcard.com/report/github.com/JeremyLoy/config)
[![Coverage Status](https://coveralls.io/repos/github/JeremyLoy/config/badge.svg?branch=master)](https://coveralls.io/github/JeremyLoy/config?branch=master)
[![GitHub issues](https://img.shields.io/github/issues/JeremyLoy/config.svg)](https://github.com/JeremyLoy/config/issues)
[![license](https://img.shields.io/github/license/JeremyLoy/config.svg?maxAge=2592000)](https://github.com/JeremyLoy/config/LICENSE)
[![Release](https://img.shields.io/github/release/JeremyLoy/config.svg?label=Release)](https://github.com/JeremyLoy/config/releases)
[![Documentation](https://godoc.org/github.com/hookactions/config?status.svg)](http://godoc.org/github.com/hookactions/config)
[![CircleCI](https://circleci.com/gh/hookactions/config.svg?style=svg)](https://circleci.com/gh/hookactions/config)
[![Go Report Card](https://goreportcard.com/badge/github.com/hookactions/config)](https://goreportcard.com/report/github.com/hookactions/config)
[![license](https://img.shields.io/github/license/hookactions/config.svg?maxAge=2592000)](https://github.com/hookactions/config/LICENSE)
[![Release](https://img.shields.io/github/release/hookactions/config.svg?label=Release)](https://github.com/hookactions/config/releases)

Manage your application config as a typesafe struct in as little as two function calls.

```go
package main

import (
"context"
"fmt"

"github.com/hookactions/config"
)

type MyConfig struct {
DatabaseUrl string `config:"DATABASE_URL"`
FeatureFlag bool `config:"FEATURE_FLAG"`
Expand All @@ -20,11 +26,22 @@ type MyConfig struct {

var c MyConfig
config.FromEnv().To(&c)

fmt.Printf("%v\n", c)

// Supports AWS Secret Manager and Parameter store
// sm://my_value
// ssm://my_value

p, _ := config.NewAWSSecretManagerValuePreProcessor(context.Background(), true)
config.WithValuePreProcessor(p).FromEnv().To(&c)

fmt.Printf("%v\n", c)
```

## How It Works

Its just simple, pure stdlib.
Its just simple, pure stdlib with optional AWS support.

* A field's type determines what [strconv](https://golang.org/pkg/strconv/) function is called.
* All string conversion rules are as defined in the [strconv](https://golang.org/pkg/strconv/) package
Expand All @@ -47,9 +64,6 @@ Its just simple, pure stdlib.
* only 2 lines to configure.
* Composeable:
* Merge local files and environment variables for effortless local development.
* small:
* only stdlib
* < 180 LoC

## Design Philosophy

Expand All @@ -64,4 +78,4 @@ Feel free to use it on its own, or alongside other libraries.
* No maps. The only feature of maps not handled by structs for this usecase is dynamic keys.
* No pointer members. If you really need one, just take the address of parts of your struct.
* No pointer members. If you really need one, just take the address of parts of your struct.
98 changes: 98 additions & 0 deletions aws.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package config

import (
"context"
"regexp"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/external"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager/secretsmanageriface"
"github.com/aws/aws-sdk-go-v2/service/ssm"
"github.com/aws/aws-sdk-go-v2/service/ssm/ssmiface"
"github.com/pkg/errors"
)

var (
secretsManagerStringRe = regexp.MustCompile("^sm://")
parameterStoreStringRe = regexp.MustCompile("^ssm://")
)

func checkPrefixAndStrip(re *regexp.Regexp, s string) (string, bool) {
if re.MatchString(s) {
return re.ReplaceAllString(s, ""), true
}
return s, false
}

// NewAWSSecretManagerValuePreProcessor creates a new AWSSecretManagerValuePreProcessor with the given context and whether to decrypt parameter store values or not.
// This will load the aws config from external.LoadDefaultAWSConfig()
func NewAWSSecretManagerValuePreProcessor(ctx context.Context, decryptParameterStoreValues bool) (*AWSSecretManagerValuePreProcessor, error) {
awsConfig, err := external.LoadDefaultAWSConfig()
if err != nil {
return nil, errors.Wrap(err, "config/aws: error loading default aws config")
}

return &AWSSecretManagerValuePreProcessor{
decryptParameterStoreValues: decryptParameterStoreValues,

secretsManager: secretsmanager.New(awsConfig),
parameterStore: ssm.New(awsConfig),
ctx: ctx,
}, nil
}

// AWSSecretManagerValuePreProcessor is a ValuePreProcessor for AWS.
// Supports Secrets Manager and Parameter Store.
type AWSSecretManagerValuePreProcessor struct {
decryptParameterStoreValues bool

secretsManager secretsmanageriface.ClientAPI
parameterStore ssmiface.ClientAPI
ctx context.Context
}

// PreProcessValue pre-processes a config key/value pair.
func (p *AWSSecretManagerValuePreProcessor) PreProcessValue(key, value string) string {
return p.processConfigItem(p.ctx, key, value)
}

func (p *AWSSecretManagerValuePreProcessor) processConfigItem(ctx context.Context, key string, value string) string {
if v, ok := checkPrefixAndStrip(secretsManagerStringRe, value); ok {
return p.loadStringValueFromSecretsManager(ctx, v)
} else if v, ok := checkPrefixAndStrip(parameterStoreStringRe, v); ok {
return p.loadStringValueFromParameterStore(ctx, v, p.decryptParameterStoreValues)
}
return value
}

func (p *AWSSecretManagerValuePreProcessor) loadStringValueFromSecretsManager(ctx context.Context, name string) string {
resp, err := p.requestSecret(ctx, name)
if err != nil {
panic("config/aws/loadStringValueFromSecretsManager: error loading secret, " + err.Error())
}

return *resp.SecretString
}

func (p *AWSSecretManagerValuePreProcessor) requestSecret(ctx context.Context, name string) (*secretsmanager.GetSecretValueResponse, error) {
input := &secretsmanager.GetSecretValueInput{SecretId: aws.String(name)}
return p.secretsManager.GetSecretValueRequest(input).Send(ctx)
}

func (p *AWSSecretManagerValuePreProcessor) loadStringValueFromParameterStore(ctx context.Context, name string, decrypt bool) string {
resp, err := p.requestParameter(ctx, name, decrypt)
if err != nil {
panic("config/aws/loadStringValueFromParameterStore: error loading value, " + err.Error())
}

return *resp.Parameter.Value
}

func (p *AWSSecretManagerValuePreProcessor) requestParameter(ctx context.Context, name string, decrypt bool) (*ssm.GetParameterResponse, error) {
input := &ssm.GetParameterInput{Name: aws.String(name), WithDecryption: aws.Bool(decrypt)}
return p.parameterStore.GetParameterRequest(input).Send(ctx)
}

// compile time assertion
var _ ValuePreProcessor = (*AWSSecretManagerValuePreProcessor)(nil)
141 changes: 141 additions & 0 deletions aws_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package config

import (
"context"
"encoding/base64"
"net/http"
"testing"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager/secretsmanageriface"
"github.com/aws/aws-sdk-go-v2/service/ssm"
"github.com/aws/aws-sdk-go-v2/service/ssm/ssmiface"
"github.com/stretchr/testify/assert"
)

type mockSecretManagerClient struct {
secretsmanageriface.ClientAPI

checkInput func(*secretsmanager.GetSecretValueInput)
stringValue *string
binaryValue []byte
}

func (m *mockSecretManagerClient) GetSecretValueRequest(in *secretsmanager.GetSecretValueInput) secretsmanager.GetSecretValueRequest {
if m.checkInput != nil {
m.checkInput(in)
}

req := &aws.Request{
Data: &secretsmanager.GetSecretValueOutput{
SecretString: m.stringValue,
SecretBinary: m.binaryValue,
},
HTTPRequest: new(http.Request),
}
return secretsmanager.GetSecretValueRequest{Request: req, Input: in, Copy: m.GetSecretValueRequest}
}

type mockParameterStoreClient struct {
ssmiface.ClientAPI

checkInput func(*ssm.GetParameterInput)
stringValue *string
binaryValue []byte
}

func (m *mockParameterStoreClient) GetParameterRequest(in *ssm.GetParameterInput) ssm.GetParameterRequest {
if m.checkInput != nil {
m.checkInput(in)
}

var value *string

if m.stringValue != nil {
value = m.stringValue
} else if m.binaryValue != nil {
value = aws.String(base64.StdEncoding.EncodeToString(m.binaryValue))
}

req := &aws.Request{
Data: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Value: value,
},
},
HTTPRequest: new(http.Request),
}
return ssm.GetParameterRequest{Request: req, Input: in, Copy: m.GetParameterRequest}
}

func TestAWSSecretManagerValuePreProcessor_PreProcessValue(t *testing.T) {
ctx := context.Background()

t.Run("NonPrefixedValues", func(t *testing.T) {
p := AWSSecretManagerValuePreProcessor{}

assert.Equal(t, "bar", p.PreProcessValue("FOO_1", "bar"))
assert.Equal(t, "test", p.PreProcessValue("FOO_BAR_BAZ", "test"))
})

t.Run("SecretsManager", func(t *testing.T) {
manager := &mockSecretManagerClient{}

p := &AWSSecretManagerValuePreProcessor{
decryptParameterStoreValues: true,
secretsManager: manager,
ctx: ctx,
}

t.Run("Simple", func(t *testing.T) {
manager.checkInput = func(input *secretsmanager.GetSecretValueInput) {
assert.Equal(t, "foo_bar", *input.SecretId)
}
manager.stringValue = aws.String("baz")

assert.Equal(t, "baz", p.PreProcessValue("FOO", "sm://foo_bar"))
})

// "complex" in the sense that this would break using strings.TrimPrefix(...)
t.Run("Complex", func(t *testing.T) {
manager.checkInput = func(input *secretsmanager.GetSecretValueInput) {
assert.Equal(t, "small_foo_bar", *input.SecretId)
}
manager.stringValue = aws.String("baz")

assert.Equal(t, "baz", p.PreProcessValue("FOO", "sm://small_foo_bar"))
})
})

t.Run("ParameterStore", func(t *testing.T) {
storeClient := &mockParameterStoreClient{}

p := &AWSSecretManagerValuePreProcessor{
decryptParameterStoreValues: true,
parameterStore: storeClient,
ctx: ctx,
}

t.Run("Simple", func(t *testing.T) {
storeClient.checkInput = func(input *ssm.GetParameterInput) {
assert.Equal(t, "foo_bar", *input.Name)
assert.True(t, *input.WithDecryption)
}
storeClient.stringValue = aws.String("baz")

assert.Equal(t, "baz", p.PreProcessValue("FOO", "ssm://foo_bar"))
})

// "complex" in the sense that this would break using strings.TrimPrefix(...)
t.Run("Complex", func(t *testing.T) {
storeClient.checkInput = func(input *ssm.GetParameterInput) {
assert.Equal(t, "ssmall_foo_bar", *input.Name)
assert.True(t, *input.WithDecryption)
}
storeClient.stringValue = aws.String("baz")

assert.Equal(t, "baz", p.PreProcessValue("FOO", "ssm://ssmall_foo_bar"))
})
})
}
Loading

0 comments on commit 4490d12

Please sign in to comment.