Add support for specifying key to fetch from AWS Secrets Manager

This commit is contained in:
Juan Mesa 2020-05-15 13:50:33 +02:00
parent e3635566eb
commit 55fa3e1b0b
4 changed files with 281 additions and 7 deletions

View File

@ -0,0 +1,97 @@
// Package secretsmanager provide methods to get data from
// AWS Secret Manager
package secretsmanager
import (
"encoding/json"
"errors"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
)
// SecretsManager returns a representation of the Secrets Manager API
func (c *Client) SecretsManager() secretsmanageriface.SecretsManagerAPI {
return c.api
}
// New creates an AWS Session Manager Client
func New(config *AWSConfig) *Client {
c := &Client{
config: config,
}
s := c.newSession(config)
c.api = secretsmanager.New(s)
return c
}
func (c *Client) newSession(config *AWSConfig) *session.Session {
// Initialize config with error verbosity
sess := aws.NewConfig().WithCredentialsChainVerboseErrors(true)
if config.Region != "" {
sess = sess.WithRegion(config.Region)
}
opts := session.Options{
Config: *sess,
}
return session.Must(session.NewSessionWithOptions(opts))
}
// GetSecret return an AWS Secret Manager secret
// in plain text from a given secret name
func (c *Client) GetSecret(spec *SecretSpec) (string, error) {
params := &secretsmanager.GetSecretValueInput{
SecretId: aws.String(spec.Name),
VersionStage: aws.String("AWSCURRENT"),
}
resp, err := c.api.GetSecretValue(params)
if err != nil {
return "", err
}
if resp.SecretString == nil {
return "", errors.New("Secret is not string")
}
secret := SecretString{
Name: *resp.Name,
SecretString: *resp.SecretString,
}
value, err := getSecretValue(&secret, spec)
if err != nil {
return "", err
}
return value, nil
}
func getSecretValue(s *SecretString, spec *SecretSpec) (string, error) {
var secretValue map[string]string
blob := []byte(s.SecretString)
err := json.Unmarshal(blob, &secretValue)
if err != nil {
return "", err
}
// If key is not set then return first value stored in secret
if spec.Key == "" {
for _, v := range secretValue {
return v, nil
}
}
if v, ok := secretValue[spec.Key]; ok {
return v, nil
}
return "", errors.New("No secret found")
}

View File

@ -0,0 +1,121 @@
package secretsmanager
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
)
type mockedSecret struct {
secretsmanageriface.SecretsManagerAPI
Resp secretsmanager.GetSecretValueOutput
}
// GetSecret return mocked secret value
func (m mockedSecret) GetSecretValue(in *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) {
return &m.Resp, nil
}
func TestGetSecret(t *testing.T) {
testCases := []struct {
arg *SecretSpec
mock secretsmanager.GetSecretValueOutput
want string
ok bool
}{
{
arg: &SecretSpec{Name: "test/secret"},
mock: secretsmanager.GetSecretValueOutput{
Name: aws.String("test/secret"),
SecretString: aws.String(`{"key": "test"}`),
},
want: "test",
ok: true,
},
{
arg: &SecretSpec{
Name: "test/secret",
Key: "key",
},
mock: secretsmanager.GetSecretValueOutput{
Name: aws.String("test/secret"),
SecretString: aws.String(`{"key": "test"}`),
},
want: "test",
ok: true,
},
{
arg: &SecretSpec{
Name: "test/secret",
Key: "second_key",
},
mock: secretsmanager.GetSecretValueOutput{
Name: aws.String("test/secret"),
SecretString: aws.String(`{"first_key": "first_val", "second_key": "second_val"}`),
},
want: "second_val",
ok: true,
},
{
arg: &SecretSpec{
Name: "test/secret",
},
mock: secretsmanager.GetSecretValueOutput{
Name: aws.String("test/secret"),
SecretString: aws.String(`{"first_key": "first_val", "second_key": "second_val"}`),
},
want: "first_val",
ok: true,
},
{
arg: &SecretSpec{
Name: "test/secret",
Key: "nonexistent",
},
mock: secretsmanager.GetSecretValueOutput{
Name: aws.String("test/secret"),
SecretString: aws.String(`{"key": "test"}`),
},
ok: false,
},
{
arg: &SecretSpec{
Name: "test/secret",
Key: "nonexistent",
},
mock: secretsmanager.GetSecretValueOutput{
Name: aws.String("test/secret"),
SecretString: aws.String(`{"first_key": "first_val", "second_key": "second_val"}`),
},
ok: false,
},
{
arg: &SecretSpec{
Name: "test/secret",
Key: "nonexistent",
},
mock: secretsmanager.GetSecretValueOutput{},
ok: false,
},
}
for _, test := range testCases {
c := &Client{
api: mockedSecret{Resp: test.mock},
}
got, err := c.GetSecret(test.arg)
if test.ok {
if got != test.want {
t.Fatalf("want %v, got %v, error %v, using arg %v", test.want, got, err, test.arg)
}
}
if !test.ok {
if err == nil {
t.Fatalf("error expected but got %q, using arg %v", err, test.arg)
}
}
t.Logf("arg (%v), want %v, got %v, err %v", test.arg, test.want, got, err)
}
}

View File

@ -0,0 +1,36 @@
package secretsmanager
import (
"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
)
// AWSConfig store configuration used to initialize
// secrets manager client.
type AWSConfig struct {
Region string
}
// SecretSpec represent specs of secret to be searched
// If Key field is not set then package will return first
// secret key stored in secret name.
//
// maps to ClusterConfig
type SecretSpec struct {
Name string
Key string
}
// Client represents an AWS Secrets Manager client
//
// maps to ProviderServices
type Client struct {
config *AWSConfig
api secretsmanageriface.SecretsManagerAPI
}
// SecretString is a concret representation
// of an AWS Secrets Manager Secret String
type SecretString struct {
Name string
SecretString string
}

View File

@ -10,13 +10,14 @@ import (
"text/template"
"time"
awssmapi "github.com/hashicorp/packer/template/interpolate/aws/secretsmanager"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/packer/common/uuid"
"github.com/hashicorp/packer/helper/common"
"github.com/hashicorp/packer/version"
vaultapi "github.com/hashicorp/vault/api"
strftime "github.com/jehiah/go-strftime"
awssmapi "github.com/overdrive3000/secretsmanager"
)
// InitTime is the UTC time when this package was initialized. It is
@ -327,24 +328,43 @@ func funcGenVault(ctx *Context) interface{} {
}
func funcGenAwsSecrets(ctx *Context) interface{} {
return func(name string) (string, error) {
return func(secret ...string) (string, error) {
if !ctx.EnableEnv {
// The error message doesn't have to be that detailed since
// semantic checks should catch this.
return "", errors.New("AWS Secrets Manager vars are only allowed in the variables section")
}
// Check if at leas 1 parameter has been used
if len(secret) == 0 {
return "", errors.New("At least one parameter must be used")
}
// client uses AWS SDK CredentialChain method. So,credentials can
// be loaded from credential file, environment variables, or IAM
// roles.
client, err := awssmapi.New()
if err != nil {
return "", fmt.Errorf("Error getting AWS Secrets Manager client: %s", err)
client := awssmapi.New(
&awssmapi.AWSConfig{},
)
var name, key string
name = secret[0]
// key is optional if not used we fetch the first
// value stored in given secret. If more that two parameters
// are passed we take second param and ignore the others
if len(secret) > 1 {
key = secret[1]
}
secret, err := client.GetSecret(name)
spec := &awssmapi.SecretSpec{
Name: name,
Key: key,
}
s, err := client.GetSecret(spec)
if err != nil {
return "", fmt.Errorf("Error getting secret: %s", err)
}
return secret, nil
return s, nil
}
}