Add support for specifying key to fetch from AWS Secrets Manager
This commit is contained in:
parent
e3635566eb
commit
55fa3e1b0b
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue