From 6d4fae0f2d23bbadc9a0a0e187f5158b43d695b7 Mon Sep 17 00:00:00 2001 From: Adrien Delorme Date: Tue, 20 Oct 2020 16:21:40 +0200 Subject: [PATCH] Add HCL2 aws_secretsmanager function (#10124) * refactor aws get secrets function out to reuse it else where * add aws_secretsmanager func and docs for HCL2 * fix GetSecret: allow to pick secret version --- common/template/funcs.go | 21 +++ hcl2template/function/aws_secretetkey.go | 39 ++++ hcl2template/functions.go | 169 +++++++++--------- .../aws/secretsmanager/secretsmanager.go | 3 + template/interpolate/funcs.go | 40 +---- website/data/docs-navigation.js | 3 +- .../contextual/aws_secretsmanager.mdx | 56 ++++++ 7 files changed, 215 insertions(+), 116 deletions(-) create mode 100644 hcl2template/function/aws_secretetkey.go create mode 100644 website/pages/docs/from-1.5/functions/contextual/aws_secretsmanager.mdx diff --git a/common/template/funcs.go b/common/template/funcs.go index fd86e1ade..06911cfdb 100644 --- a/common/template/funcs.go +++ b/common/template/funcs.go @@ -9,6 +9,7 @@ import ( "sync" consulapi "github.com/hashicorp/consul/api" + awssmapi "github.com/hashicorp/packer/template/interpolate/aws/secretsmanager" vaultapi "github.com/hashicorp/vault/api" ) @@ -86,3 +87,23 @@ func Consul(k string) (string, error) { return value, nil } + +func GetAWSSecret(name, key string) (string, error) { + // Check if at least 1 parameter has been used + if len(name) == 0 { + return "", errors.New("At least one secret name must be provided") + } + // client uses AWS SDK CredentialChain method. So,credentials can + // be loaded from credential file, environment variables, or IAM + // roles. + client := awssmapi.New( + &awssmapi.AWSConfig{}, + ) + + spec := &awssmapi.SecretSpec{ + Name: name, + Key: key, + } + + return client.GetSecret(spec) +} diff --git a/hcl2template/function/aws_secretetkey.go b/hcl2template/function/aws_secretetkey.go new file mode 100644 index 000000000..a76482464 --- /dev/null +++ b/hcl2template/function/aws_secretetkey.go @@ -0,0 +1,39 @@ +package function + +import ( + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + + commontpl "github.com/hashicorp/packer/common/template" +) + +// AWSSecret constructs a function that retrieves secrets from aws secrets +// manager. If Key field is not set then we will return first secret key stored +// in secret name. +var AWSSecret = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "name", + Type: cty.String, + AllowNull: false, + AllowUnknown: false, + }, + { + Name: "key", + Type: cty.String, + AllowNull: true, + AllowUnknown: false, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + name := args[0].AsString() + var key string + if !args[1].IsNull() && args[1].IsWhollyKnown() { + key = args[1].AsString() + } + val, err := commontpl.GetAWSSecret(name, key) + + return cty.StringVal(val), err + }, +}) diff --git a/hcl2template/functions.go b/hcl2template/functions.go index 3f3e7a858..f91abdab7 100644 --- a/hcl2template/functions.go +++ b/hcl2template/functions.go @@ -28,90 +28,91 @@ import ( func Functions(basedir string) map[string]function.Function { funcs := map[string]function.Function{ - "abs": stdlib.AbsoluteFunc, - "abspath": filesystem.AbsPathFunc, - "basename": filesystem.BasenameFunc, - "base64decode": encoding.Base64DecodeFunc, - "base64encode": encoding.Base64EncodeFunc, - "bcrypt": crypto.BcryptFunc, - "can": tryfunc.CanFunc, - "ceil": stdlib.CeilFunc, - "chomp": stdlib.ChompFunc, - "chunklist": stdlib.ChunklistFunc, - "cidrhost": cidr.HostFunc, - "cidrnetmask": cidr.NetmaskFunc, - "cidrsubnet": cidr.SubnetFunc, - "cidrsubnets": cidr.SubnetsFunc, - "coalesce": collection.CoalesceFunc, - "coalescelist": stdlib.CoalesceListFunc, - "compact": stdlib.CompactFunc, - "concat": stdlib.ConcatFunc, - "consul_key": pkrfunction.ConsulFunc, - "contains": stdlib.ContainsFunc, - "convert": typeexpr.ConvertFunc, - "csvdecode": stdlib.CSVDecodeFunc, - "dirname": filesystem.DirnameFunc, - "distinct": stdlib.DistinctFunc, - "element": stdlib.ElementFunc, - "file": filesystem.MakeFileFunc(basedir, false), - "fileexists": filesystem.MakeFileExistsFunc(basedir), - "fileset": filesystem.MakeFileSetFunc(basedir), - "flatten": stdlib.FlattenFunc, - "floor": stdlib.FloorFunc, - "format": stdlib.FormatFunc, - "formatdate": stdlib.FormatDateFunc, - "formatlist": stdlib.FormatListFunc, - "indent": stdlib.IndentFunc, - "index": stdlib.IndexFunc, - "join": stdlib.JoinFunc, - "jsondecode": stdlib.JSONDecodeFunc, - "jsonencode": stdlib.JSONEncodeFunc, - "keys": stdlib.KeysFunc, - "length": stdlib.LengthFunc, - "log": stdlib.LogFunc, - "lookup": stdlib.LookupFunc, - "lower": stdlib.LowerFunc, - "max": stdlib.MaxFunc, - "md5": crypto.Md5Func, - "merge": stdlib.MergeFunc, - "min": stdlib.MinFunc, - "parseint": stdlib.ParseIntFunc, - "pathexpand": filesystem.PathExpandFunc, - "pow": stdlib.PowFunc, - "range": stdlib.RangeFunc, - "reverse": stdlib.ReverseFunc, - "replace": stdlib.ReplaceFunc, - "regex_replace": stdlib.RegexReplaceFunc, - "rsadecrypt": crypto.RsaDecryptFunc, - "setintersection": stdlib.SetIntersectionFunc, - "setproduct": stdlib.SetProductFunc, - "setunion": stdlib.SetUnionFunc, - "sha1": crypto.Sha1Func, - "sha256": crypto.Sha256Func, - "sha512": crypto.Sha512Func, - "signum": stdlib.SignumFunc, - "slice": stdlib.SliceFunc, - "sort": stdlib.SortFunc, - "split": stdlib.SplitFunc, - "strrev": stdlib.ReverseFunc, - "substr": stdlib.SubstrFunc, - "timestamp": pkrfunction.TimestampFunc, - "timeadd": stdlib.TimeAddFunc, - "title": stdlib.TitleFunc, - "trim": stdlib.TrimFunc, - "trimprefix": stdlib.TrimPrefixFunc, - "trimspace": stdlib.TrimSpaceFunc, - "trimsuffix": stdlib.TrimSuffixFunc, - "try": tryfunc.TryFunc, - "upper": stdlib.UpperFunc, - "urlencode": encoding.URLEncodeFunc, - "uuidv4": uuid.V4Func, - "uuidv5": uuid.V5Func, - "values": stdlib.ValuesFunc, - "vault": pkrfunction.VaultFunc, - "yamldecode": ctyyaml.YAMLDecodeFunc, - "yamlencode": ctyyaml.YAMLEncodeFunc, - "zipmap": stdlib.ZipmapFunc, + "abs": stdlib.AbsoluteFunc, + "abspath": filesystem.AbsPathFunc, + "aws_secretsmanager": pkrfunction.AWSSecret, + "basename": filesystem.BasenameFunc, + "base64decode": encoding.Base64DecodeFunc, + "base64encode": encoding.Base64EncodeFunc, + "bcrypt": crypto.BcryptFunc, + "can": tryfunc.CanFunc, + "ceil": stdlib.CeilFunc, + "chomp": stdlib.ChompFunc, + "chunklist": stdlib.ChunklistFunc, + "cidrhost": cidr.HostFunc, + "cidrnetmask": cidr.NetmaskFunc, + "cidrsubnet": cidr.SubnetFunc, + "cidrsubnets": cidr.SubnetsFunc, + "coalesce": collection.CoalesceFunc, + "coalescelist": stdlib.CoalesceListFunc, + "compact": stdlib.CompactFunc, + "concat": stdlib.ConcatFunc, + "consul_key": pkrfunction.ConsulFunc, + "contains": stdlib.ContainsFunc, + "convert": typeexpr.ConvertFunc, + "csvdecode": stdlib.CSVDecodeFunc, + "dirname": filesystem.DirnameFunc, + "distinct": stdlib.DistinctFunc, + "element": stdlib.ElementFunc, + "file": filesystem.MakeFileFunc(basedir, false), + "fileexists": filesystem.MakeFileExistsFunc(basedir), + "fileset": filesystem.MakeFileSetFunc(basedir), + "flatten": stdlib.FlattenFunc, + "floor": stdlib.FloorFunc, + "format": stdlib.FormatFunc, + "formatdate": stdlib.FormatDateFunc, + "formatlist": stdlib.FormatListFunc, + "indent": stdlib.IndentFunc, + "index": stdlib.IndexFunc, + "join": stdlib.JoinFunc, + "jsondecode": stdlib.JSONDecodeFunc, + "jsonencode": stdlib.JSONEncodeFunc, + "keys": stdlib.KeysFunc, + "length": stdlib.LengthFunc, + "log": stdlib.LogFunc, + "lookup": stdlib.LookupFunc, + "lower": stdlib.LowerFunc, + "max": stdlib.MaxFunc, + "md5": crypto.Md5Func, + "merge": stdlib.MergeFunc, + "min": stdlib.MinFunc, + "parseint": stdlib.ParseIntFunc, + "pathexpand": filesystem.PathExpandFunc, + "pow": stdlib.PowFunc, + "range": stdlib.RangeFunc, + "reverse": stdlib.ReverseFunc, + "replace": stdlib.ReplaceFunc, + "regex_replace": stdlib.RegexReplaceFunc, + "rsadecrypt": crypto.RsaDecryptFunc, + "setintersection": stdlib.SetIntersectionFunc, + "setproduct": stdlib.SetProductFunc, + "setunion": stdlib.SetUnionFunc, + "sha1": crypto.Sha1Func, + "sha256": crypto.Sha256Func, + "sha512": crypto.Sha512Func, + "signum": stdlib.SignumFunc, + "slice": stdlib.SliceFunc, + "sort": stdlib.SortFunc, + "split": stdlib.SplitFunc, + "strrev": stdlib.ReverseFunc, + "substr": stdlib.SubstrFunc, + "timestamp": pkrfunction.TimestampFunc, + "timeadd": stdlib.TimeAddFunc, + "title": stdlib.TitleFunc, + "trim": stdlib.TrimFunc, + "trimprefix": stdlib.TrimPrefixFunc, + "trimspace": stdlib.TrimSpaceFunc, + "trimsuffix": stdlib.TrimSuffixFunc, + "try": tryfunc.TryFunc, + "upper": stdlib.UpperFunc, + "urlencode": encoding.URLEncodeFunc, + "uuidv4": uuid.V4Func, + "uuidv5": uuid.V5Func, + "values": stdlib.ValuesFunc, + "vault": pkrfunction.VaultFunc, + "yamldecode": ctyyaml.YAMLDecodeFunc, + "yamlencode": ctyyaml.YAMLEncodeFunc, + "zipmap": stdlib.ZipmapFunc, } return funcs diff --git a/template/interpolate/aws/secretsmanager/secretsmanager.go b/template/interpolate/aws/secretsmanager/secretsmanager.go index 67ec3b90b..ae92c289d 100644 --- a/template/interpolate/aws/secretsmanager/secretsmanager.go +++ b/template/interpolate/aws/secretsmanager/secretsmanager.go @@ -53,6 +53,9 @@ func (c *Client) GetSecret(spec *SecretSpec) (string, error) { SecretId: aws.String(spec.Name), VersionStage: aws.String("AWSCURRENT"), } + if spec.Name != "" { + params.VersionStage = aws.String(spec.Key) + } resp, err := c.api.GetSecretValue(params) if err != nil { diff --git a/template/interpolate/funcs.go b/template/interpolate/funcs.go index f6fae509d..8ec9b8c1f 100644 --- a/template/interpolate/funcs.go +++ b/template/interpolate/funcs.go @@ -13,7 +13,6 @@ import ( commontpl "github.com/hashicorp/packer/common/template" "github.com/hashicorp/packer/common/uuid" "github.com/hashicorp/packer/helper/common" - awssmapi "github.com/hashicorp/packer/template/interpolate/aws/secretsmanager" "github.com/hashicorp/packer/version" strftime "github.com/jehiah/go-strftime" ) @@ -280,37 +279,16 @@ func funcGenAwsSecrets(ctx *Context) interface{} { // semantic checks should catch this. return "", errors.New("AWS Secrets Manager is only allowed in the variables section") } - - // Check if at least 1 parameter has been used - if len(secret) == 0 { - return "", errors.New("At least one secret name must be provided") + switch len(secret) { + case 0: + return "", errors.New("secret name must be provided") + case 1: + return commontpl.GetAWSSecret(secret[0], "") + case 2: + return commontpl.GetAWSSecret(secret[0], secret[1]) + default: + return "", errors.New("only secret name and optional secret key can be provided.") } - // client uses AWS SDK CredentialChain method. So,credentials can - // be loaded from credential file, environment variables, or IAM - // roles. - 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 than two parameters - // are passed we take second param and ignore the others - if len(secret) > 1 { - key = secret[1] - } - - spec := &awssmapi.SecretSpec{ - Name: name, - Key: key, - } - - s, err := client.GetSecret(spec) - if err != nil { - return "", err - } - return s, nil } } diff --git a/website/data/docs-navigation.js b/website/data/docs-navigation.js index d2501f960..b4da5a875 100644 --- a/website/data/docs-navigation.js +++ b/website/data/docs-navigation.js @@ -32,8 +32,9 @@ export default [ { category: 'contextual', content: [ - 'vault', + 'aws_secretsmanager', 'consul', + 'vault', ], }, { diff --git a/website/pages/docs/from-1.5/functions/contextual/aws_secretsmanager.mdx b/website/pages/docs/from-1.5/functions/contextual/aws_secretsmanager.mdx new file mode 100644 index 000000000..483e08176 --- /dev/null +++ b/website/pages/docs/from-1.5/functions/contextual/aws_secretsmanager.mdx @@ -0,0 +1,56 @@ +--- +layout: docs +page_title: aws_secretsmanager - Functions - Configuration Language +sidebar_title: aws_secretsmanager +description: The aws_secretsmanager function retrieves secrets from Amazon secretsmanager stores. +--- + + +# `aws_secretsmanager_key` Function + +Secrets can be read from the [AWS Secrets +Manager](https://aws.amazon.com/secrets-manager/) and used within your template +as locals. + +```hcl +aws_secretsmanager(name, key) +``` + +When key is not set (`null` or empty: `""`) then `aws_secretsmanager` returns +the first secret key stored in secret `name` using the `AWSCURRENT`. + +You can either use this function in a `locals` block or directly inline where +you want to use the value. + +```hcl +locals { + // null is equivalent to "AWSCURRENT" + current_version = aws_secretsmanager("my_secret", null) +} + +source "null" "first-example" { + communicator = "none" +} + +build { + name = "my-build-name" + sources = ["null.first-example"] + + provisioner "shell-local" { + environment_vars = ["TESTVAR=${build.PackerRunUUID}"] + inline = ["echo current version is '${local.current_version}'", + "echo previous version is '${aws_secretsmanager("my_secret", "AWSPREVIOUS")}'."] + } +} +``` + +This will load the key stored at behind `my_secret` from aws secrets manager. + + +In order to use this function you have to configure valid AWS credentials using +one of the following methods: + +- [Environment Variables](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html) +- [CLI Configuration Files](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html) +- [Container Credentials](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html) +- [Instance Profile Credentials](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)