From 55fa3e1b0bf85ec138461057acfc23141a7525cb Mon Sep 17 00:00:00 2001 From: Juan Mesa Date: Fri, 15 May 2020 13:50:33 +0200 Subject: [PATCH] Add support for specifying key to fetch from AWS Secrets Manager --- .../aws/secretsmanager/secretsmanager.go | 97 ++++++++++++++ .../aws/secretsmanager/secretsmanager_test.go | 121 ++++++++++++++++++ .../interpolate/aws/secretsmanager/types.go | 36 ++++++ template/interpolate/funcs.go | 34 ++++- 4 files changed, 281 insertions(+), 7 deletions(-) create mode 100644 template/interpolate/aws/secretsmanager/secretsmanager.go create mode 100644 template/interpolate/aws/secretsmanager/secretsmanager_test.go create mode 100644 template/interpolate/aws/secretsmanager/types.go diff --git a/template/interpolate/aws/secretsmanager/secretsmanager.go b/template/interpolate/aws/secretsmanager/secretsmanager.go new file mode 100644 index 000000000..94fa26159 --- /dev/null +++ b/template/interpolate/aws/secretsmanager/secretsmanager.go @@ -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") +} diff --git a/template/interpolate/aws/secretsmanager/secretsmanager_test.go b/template/interpolate/aws/secretsmanager/secretsmanager_test.go new file mode 100644 index 000000000..985c26f05 --- /dev/null +++ b/template/interpolate/aws/secretsmanager/secretsmanager_test.go @@ -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) + } +} diff --git a/template/interpolate/aws/secretsmanager/types.go b/template/interpolate/aws/secretsmanager/types.go new file mode 100644 index 000000000..40d1276fa --- /dev/null +++ b/template/interpolate/aws/secretsmanager/types.go @@ -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 +} diff --git a/template/interpolate/funcs.go b/template/interpolate/funcs.go index 258b1888e..98696e1ee 100644 --- a/template/interpolate/funcs.go +++ b/template/interpolate/funcs.go @@ -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 } }