Skip to content

Commit

Permalink
Aws Secrets Manager data sources (hashicorp#10505)
Browse files Browse the repository at this point in the history
  • Loading branch information
sylviamoss authored Jan 22, 2021
1 parent e48a86f commit d1ada74
Show file tree
Hide file tree
Showing 24 changed files with 723 additions and 39 deletions.
2 changes: 2 additions & 0 deletions cmd/packer-plugin-amazon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/hashicorp/packer/builder/amazon/ebsvolume"
"github.com/hashicorp/packer/builder/osc/chroot"
amazonami "github.com/hashicorp/packer/datasource/amazon/ami"
"github.com/hashicorp/packer/datasource/amazon/secretsmanager"
amazonimport "github.com/hashicorp/packer/post-processor/amazon-import"
)

Expand All @@ -21,6 +22,7 @@ func main() {
pps.RegisterBuilder("ebsvolume", new(ebsvolume.Builder))
pps.RegisterPostProcessor("import", new(amazonimport.PostProcessor))
pps.RegisterDatasource("ami", new(amazonami.Datasource))
pps.RegisterDatasource("secretsmanager", new(secretsmanager.Datasource))
err := pps.Run()
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
Expand Down
4 changes: 3 additions & 1 deletion command/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import (
vsphereisobuilder "github.com/hashicorp/packer/builder/vsphere/iso"
yandexbuilder "github.com/hashicorp/packer/builder/yandex"
amazonamidatasource "github.com/hashicorp/packer/datasource/amazon/ami"
amazonsecretsmanagerdatasource "github.com/hashicorp/packer/datasource/amazon/secretsmanager"
alicloudimportpostprocessor "github.com/hashicorp/packer/post-processor/alicloud-import"
amazonimportpostprocessor "github.com/hashicorp/packer/post-processor/amazon-import"
artificepostprocessor "github.com/hashicorp/packer/post-processor/artifice"
Expand Down Expand Up @@ -214,7 +215,8 @@ var PostProcessors = map[string]packersdk.PostProcessor{
}

var Datasources = map[string]packersdk.Datasource{
"amazon-ami": new(amazonamidatasource.Datasource),
"amazon-ami": new(amazonamidatasource.Datasource),
"amazon-secretsmanager": new(amazonsecretsmanagerdatasource.Datasource),
}

var pluginRegexp = regexp.MustCompile("packer-(builder|post-processor|provisioner|datasource)-(.+)")
Expand Down
19 changes: 13 additions & 6 deletions datasource/amazon/ami/data.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//go:generate struct-markdown
//go:generate mapstructure-to-hcl2 -type DatasourceOutput,Config
package ami

Expand Down Expand Up @@ -50,12 +51,18 @@ func (d *Datasource) Configure(raws ...interface{}) error {
}

type DatasourceOutput struct {
ID string `mapstructure:"id"`
Name string `mapstructure:"name"`
CreationDate string `mapstructure:"creation_date"`
Owner string `mapstructure:"owner"`
OwnerName string `mapstructure:"owner_name"`
Tags map[string]string `mapstructure:"tags"`
// The ID of the AMI.
ID string `mapstructure:"id"`
// The name of the AMI.
Name string `mapstructure:"name"`
// The date of creation of the AMI.
CreationDate string `mapstructure:"creation_date"`
// The AWS account ID of the owner.
Owner string `mapstructure:"owner"`
// The owner alias.
OwnerName string `mapstructure:"owner_name"`
// The key/value combination of the tags assigned to the AMI.
Tags map[string]string `mapstructure:"tags"`
}

func (d *Datasource) OutputSpec() hcldec.ObjectSpec {
Expand Down
168 changes: 168 additions & 0 deletions datasource/amazon/secretsmanager/data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//go:generate struct-markdown
//go:generate mapstructure-to-hcl2 -type DatasourceOutput,Config
package secretsmanager

import (
"encoding/json"
"fmt"
"strconv"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer-plugin-sdk/hcl2helper"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/config"
awscommon "github.com/hashicorp/packer/builder/amazon/common"
"github.com/hashicorp/packer/builder/amazon/common/awserrors"
"github.com/zclconf/go-cty/cty"
)

type Datasource struct {
config Config
}

type Config struct {
// Specifies the secret containing the version that you want to retrieve.
// You can specify either the Amazon Resource Name (ARN) or the friendly name of the secret.
Name string `mapstructure:"name" required:"true"`
// Optional key for JSON secrets that contain more than one value. When set, the `value` output will
// contain the value for the provided key.
Key string `mapstructure:"key"`
// Specifies the unique identifier of the version of the secret that you want to retrieve.
// Overrides version_stage.
VersionId string `mapstructure:"version_id"`
// Specifies the secret version that you want to retrieve by the staging label attached to the version.
// Defaults to AWSCURRENT.
VersionStage string `mapstructure:"version_stage"`
awscommon.AccessConfig `mapstructure:",squash"`
}

func (d *Datasource) ConfigSpec() hcldec.ObjectSpec {
return d.config.FlatMapstructure().HCL2Spec()
}

func (d *Datasource) Configure(raws ...interface{}) error {
err := config.Decode(&d.config, nil, raws...)
if err != nil {
return err
}

var errs *packersdk.MultiError
errs = packersdk.MultiErrorAppend(errs, d.config.AccessConfig.Prepare()...)

if d.config.Name == "" {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("a 'name' must be provided"))
}

if d.config.VersionStage == "" {
d.config.VersionStage = "AWSCURRENT"
}

if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
}

type DatasourceOutput struct {
// When a [key](#key) is provided, this will be the value for that key. If a key is not provided,
// `value` will contain the first value found in the secret string.
Value string `mapstructure:"value"`
// The decrypted part of the protected secret information that
// was originally provided as a string.
SecretString string `mapstructure:"secret_string"`
// The decrypted part of the protected secret information that
// was originally provided as a binary. Base64 encoded.
SecretBinary string `mapstructure:"secret_binary"`
// The unique identifier of this version of the secret.
VersionId string `mapstructure:"version_id"`
}

func (d *Datasource) OutputSpec() hcldec.ObjectSpec {
return (&DatasourceOutput{}).FlatMapstructure().HCL2Spec()
}

func (d *Datasource) Execute() (cty.Value, error) {
session, err := d.config.Session()
if err != nil {
return cty.NullVal(cty.EmptyObject), err
}

input := &secretsmanager.GetSecretValueInput{
SecretId: aws.String(d.config.Name),
}

version := ""
if d.config.VersionId != "" {
input.VersionId = aws.String(d.config.VersionId)
version = d.config.VersionId
} else {
input.VersionStage = aws.String(d.config.VersionStage)
version = d.config.VersionStage
}

secretsApi := secretsmanager.New(session)
secret, err := secretsApi.GetSecretValue(input)
if err != nil {
if awserrors.Matches(err, secretsmanager.ErrCodeResourceNotFoundException, "") {
return cty.NullVal(cty.EmptyObject), fmt.Errorf("Secrets Manager Secret %q Version %q not found", d.config.Name, version)
}
if awserrors.Matches(err, secretsmanager.ErrCodeInvalidRequestException, "You can’t perform this operation on the secret because it was deleted") {
return cty.NullVal(cty.EmptyObject), fmt.Errorf("Secrets Manager Secret %q Version %q not found", d.config.Name, version)
}
return cty.NullVal(cty.EmptyObject), fmt.Errorf("error reading Secrets Manager Secret Version: %s", err)
}

value, err := getSecretValue(aws.StringValue(secret.SecretString), d.config.Key)
if err != nil {
return cty.NullVal(cty.EmptyObject), fmt.Errorf("error to get secret value: %q", err.Error())
}

versionId := aws.StringValue(secret.VersionId)
output := DatasourceOutput{
Value: value,
SecretString: aws.StringValue(secret.SecretString),
SecretBinary: fmt.Sprintf("%s", secret.SecretBinary),
VersionId: versionId,
}
return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil
}

func getSecretValue(secretString string, key string) (string, error) {
var secretValue map[string]interface{}
blob := []byte(secretString)

//For those plaintext secrets just return the value
if json.Valid(blob) != true {
return secretString, nil
}

err := json.Unmarshal(blob, &secretValue)
if err != nil {
return "", err
}

if key == "" {
for _, v := range secretValue {
return getStringSecretValue(v)
}
}

if v, ok := secretValue[key]; ok {
return getStringSecretValue(v)
}

return "", nil
}

func getStringSecretValue(v interface{}) (string, error) {
switch valueType := v.(type) {
case string:
return valueType, nil
case float64:
return strconv.FormatFloat(valueType, 'f', 0, 64), nil
default:
return "", fmt.Errorf("Unsupported secret value type: %T", valueType)
}
}
99 changes: 99 additions & 0 deletions datasource/amazon/secretsmanager/data.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d1ada74

Please sign in to comment.