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

Commit

Permalink
awsenv (#4)
Browse files Browse the repository at this point in the history
* awsenv

* tests

* update makefile and fix imports
  • Loading branch information
tizz98 authored Jun 11, 2019
1 parent 1000253 commit 183f9c2
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 0 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
test:
cd aws && go vet ./... && go test -v ./... -race -cover
cd awsEnv && go vet ./... && go test -v ./... -race -cover
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,38 @@ go run main.go
- _Note_: decryption of the value is automatically requested.
- `ssmb64://` – Get base64 encoded binary value from [parameter store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html)
- _Note_: decryption of the value is automatically requested.

## AWS via Env

Pre-process config values by reading secrets from AWS.

```go
// main.go
package main

import (
"context"
"fmt"
"os"

"github.com/hookactions/fig/awsEnv"
)

func main() {
fig, err := awsEnv.New()
if err != nil {
panic(err)
}

fmt.Println(fig.GetEnv(context.Background(), "MY_VAR"))
}
```

```bash
MY_VAR=sm://foo go run main.go
```

### Supported prefixes
- `sm://` – Get string value from [secrets manager](https://aws.amazon.com/secrets-manager/)
- `ssm://` – Get string value from [parameter store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html)
- _Note_: decryption of the value is automatically requested.
92 changes: 92 additions & 0 deletions awsEnv/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package awsEnv

import (
"context"
"os"
"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
}

type Fig struct {
DecryptParameterStoreValues bool

secretsManager secretsmanageriface.ClientAPI
parameterStore ssmiface.ClientAPI
}

func New() (*Fig, error) {
awsConfig, err := external.LoadDefaultAWSConfig()
if err != nil {
return nil, errors.Wrap(err, "fig/awsEnv: error loading default aws config")
}

fig := &Fig{
DecryptParameterStoreValues: true,

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

return fig, nil
}

func (f *Fig) GetEnv(ctx context.Context, key string) string {
value := os.Getenv(key)
return f.processConfigItem(ctx, key, value)
}

func (f *Fig) processConfigItem(ctx context.Context, key string, value string) string {
if v, ok := checkPrefixAndStrip(secretsManagerStringRe, value); ok {
return f.LoadStringValueFromSecretsManager(ctx, v)
} else if v, ok := checkPrefixAndStrip(parameterStoreStringRe, v); ok {
return f.LoadStringValueFromParameterStore(ctx, v, f.DecryptParameterStoreValues)
}
return value
}

func (f *Fig) LoadStringValueFromSecretsManager(ctx context.Context, name string) string {
resp, err := f.requestSecret(ctx, name)
if err != nil {
panic("fig/aws/LoadStringValueFromSecretsManager: error loading secret, " + err.Error())
}

return *resp.SecretString
}

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

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

return *resp.Parameter.Value
}

func (f *Fig) requestParameter(ctx context.Context, name string, decrypt bool) (*ssm.GetParameterResponse, error) {
input := &ssm.GetParameterInput{Name: aws.String(name), WithDecryption: aws.Bool(decrypt)}
return f.parameterStore.GetParameterRequest(input).Send(ctx)
}
164 changes: 164 additions & 0 deletions awsEnv/env_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package awsEnv

import (
"context"
"encoding/base64"
"net/http"
"os"
"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"
"github.com/stretchr/testify/require"
)

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 TestFig_GetEnv(t *testing.T) {
t.Run("NonPrefixedValues", func(t *testing.T) {
fig := &Fig{}
ctx := context.Background()

require.NoError(t, os.Setenv("FOO_1", "bar"))
require.NoError(t, os.Setenv("FOO_BAR_BAZ", "test"))

defer os.Unsetenv("FOO_1")
defer os.Unsetenv("FOO_BAR_BAZ")

assert.Equal(t, "bar", fig.GetEnv(ctx, "FOO_1"))
assert.Equal(t, "test", fig.GetEnv(ctx, "FOO_BAR_BAZ"))
})

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

fig := &Fig{
DecryptParameterStoreValues: true,
secretsManager: manager,
}
ctx := context.Background()

t.Run("String", func(t *testing.T) {
t.Run("Simple", func(t *testing.T) {
require.NoError(t, os.Setenv("foo", "sm://foo_bar"))
defer os.Unsetenv("foo")

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

assert.Equal(t, "baz", fig.GetEnv(ctx, "foo"))
})

// "complex" in the sense that this would break using strings.TrimPrefix(...)
t.Run("Complex", func(t *testing.T) {
require.NoError(t, os.Setenv("foo", "sm://small_foo_bar"))
defer os.Unsetenv("foo")

manager.checkInput = func(input *secretsmanager.GetSecretValueInput) {
assert.Equal(t, "small_foo_bar", *input.SecretId)
}
manager.stringValue = aws.String("baz")

assert.Equal(t, "baz", fig.GetEnv(ctx, "foo"))
})
})
})

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

fig := &Fig{
DecryptParameterStoreValues: true,
parameterStore: storeClient,
}
ctx := context.Background()

t.Run("String", func(t *testing.T) {
t.Run("Simple", func(t *testing.T) {
require.NoError(t, os.Setenv("foo", "ssm://foo_bar"))
defer os.Unsetenv("foo")

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", fig.GetEnv(ctx, "foo"))
})

// "complex" in the sense that this would break using strings.TrimPrefix(...)
t.Run("Complex", func(t *testing.T) {
require.NoError(t, os.Setenv("foo", "ssm://ssmall_foo_bar"))
defer os.Unsetenv("foo")

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", fig.GetEnv(ctx, "foo"))
})
})
})
}
9 changes: 9 additions & 0 deletions awsEnv/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/hookactions/fig/env

go 1.12

require (
github.com/aws/aws-sdk-go-v2 v0.9.0
github.com/pkg/errors v0.8.1
github.com/stretchr/testify v1.2.2
)
19 changes: 19 additions & 0 deletions awsEnv/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
github.com/aws/aws-sdk-go-v2 v0.9.0 h1:dWtJKGRFv3UZkMBQaIzMsF0/y4ge3iQPWTzeC4r/vl4=
github.com/aws/aws-sdk-go-v2 v0.9.0/go.mod h1:sa1GePZ/LfBGI4dSq30f6uR4Tthll8axxtEPvlpXZ8U=
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/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=

0 comments on commit 183f9c2

Please sign in to comment.