From ff9af4c90bc409a2ce2048481de38183217fa68d Mon Sep 17 00:00:00 2001 From: Kit Ewbank Date: Sat, 22 Apr 2017 21:51:20 -0400 Subject: [PATCH] Add tagging support to the 'aws_lambda_function' resource. (#13873) --- .../aws/resource_aws_lambda_function.go | 13 ++ .../aws/resource_aws_lambda_function_test.go | 113 ++++++++++++++++++ builtin/providers/aws/tagsGeneric.go | 69 +++++++++++ builtin/providers/aws/tagsGeneric_test.go | 73 +++++++++++ builtin/providers/aws/tagsLambda.go | 50 ++++++++ .../aws/r/lambda_function.html.markdown | 1 + 6 files changed, 319 insertions(+) create mode 100644 builtin/providers/aws/tagsGeneric.go create mode 100644 builtin/providers/aws/tagsGeneric_test.go create mode 100644 builtin/providers/aws/tagsLambda.go diff --git a/builtin/providers/aws/resource_aws_lambda_function.go b/builtin/providers/aws/resource_aws_lambda_function.go index 4eedd77a35dd..4a1e72023e52 100644 --- a/builtin/providers/aws/resource_aws_lambda_function.go +++ b/builtin/providers/aws/resource_aws_lambda_function.go @@ -175,6 +175,8 @@ func resourceAwsLambdaFunction() *schema.Resource { Optional: true, ValidateFunc: validateArn, }, + + "tags": tagsSchema(), }, } } @@ -291,6 +293,10 @@ func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) e params.KMSKeyArn = aws.String(v.(string)) } + if v, exists := d.GetOk("tags"); exists { + params.Tags = tagsFromMapGeneric(v.(map[string]interface{})) + } + // IAM profiles can take ~10 seconds to propagate in AWS: // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console // Error creating Lambda function: InvalidParameterValueException: The role defined for the task cannot be assumed by Lambda. @@ -353,6 +359,7 @@ func resourceAwsLambdaFunctionRead(d *schema.ResourceData, meta interface{}) err d.Set("runtime", function.Runtime) d.Set("timeout", function.Timeout) d.Set("kms_key_arn", function.KMSKeyArn) + d.Set("tags", tagsToMapGeneric(getFunctionOutput.Tags)) config := flattenLambdaVpcConfigResponse(function.VpcConfig) log.Printf("[INFO] Setting Lambda %s VPC config %#v from API", d.Id(), config) @@ -448,6 +455,12 @@ func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) e d.Partial(true) + arn := d.Get("arn").(string) + if tagErr := setTagsLambda(conn, d, arn); tagErr != nil { + return tagErr + } + d.SetPartial("tags") + if d.HasChange("filename") || d.HasChange("source_code_hash") || d.HasChange("s3_bucket") || d.HasChange("s3_key") || d.HasChange("s3_object_version") { codeReq := &lambda.UpdateFunctionCodeInput{ FunctionName: aws.String(d.Id()), diff --git a/builtin/providers/aws/resource_aws_lambda_function_test.go b/builtin/providers/aws/resource_aws_lambda_function_test.go index 26bcc78e39be..4679c60fc899 100644 --- a/builtin/providers/aws/resource_aws_lambda_function_test.go +++ b/builtin/providers/aws/resource_aws_lambda_function_test.go @@ -582,6 +582,74 @@ func TestAccAWSLambdaFunction_runtimeValidation_java8(t *testing.T) { }) } +func TestAccAWSLambdaFunction_tags(t *testing.T) { + var conf lambda.GetFunctionOutput + + rSt := acctest.RandString(5) + rName := fmt.Sprintf("tf_test_%s", rSt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLambdaFunctionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLambdaConfigBasic(rName, rSt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_test", rName, &conf), + testAccCheckAwsLambdaFunctionName(&conf, rName), + testAccCheckAwsLambdaFunctionArnHasSuffix(&conf, ":"+rName), + resource.TestCheckNoResourceAttr("aws_lambda_function.lambda_function_test", "tags"), + ), + }, + { + Config: testAccAWSLambdaConfigTags(rName, rSt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_test", rName, &conf), + testAccCheckAwsLambdaFunctionName(&conf, rName), + testAccCheckAwsLambdaFunctionArnHasSuffix(&conf, ":"+rName), + resource.TestCheckResourceAttr("aws_lambda_function.lambda_function_test", "tags.%", "2"), + resource.TestCheckResourceAttr("aws_lambda_function.lambda_function_test", "tags.Key1", "Value One"), + resource.TestCheckResourceAttr("aws_lambda_function.lambda_function_test", "tags.Description", "Very interesting"), + ), + }, + { + Config: testAccAWSLambdaConfigTagsModified(rName, rSt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_test", rName, &conf), + testAccCheckAwsLambdaFunctionName(&conf, rName), + testAccCheckAwsLambdaFunctionArnHasSuffix(&conf, ":"+rName), + resource.TestCheckResourceAttr("aws_lambda_function.lambda_function_test", "tags.%", "3"), + resource.TestCheckResourceAttr("aws_lambda_function.lambda_function_test", "tags.Key1", "Value One Changed"), + resource.TestCheckResourceAttr("aws_lambda_function.lambda_function_test", "tags.Key2", "Value Two"), + resource.TestCheckResourceAttr("aws_lambda_function.lambda_function_test", "tags.Key3", "Value Three"), + ), + }, + }, + }) +} + +func TestAccAWSLambdaFunction_runtimeValidation_python36(t *testing.T) { + var conf lambda.GetFunctionOutput + rSt := acctest.RandString(5) + rName := fmt.Sprintf("tf_test_%s", rSt) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLambdaFunctionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLambdaConfigPython36Runtime(rName, rSt), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_test", rName, &conf), + resource.TestCheckResourceAttr("aws_lambda_function.lambda_function_test", "runtime", lambda.RuntimePython36), + ), + }, + }, + }) +} + func testAccCheckLambdaFunctionDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).lambdaconn @@ -1106,6 +1174,51 @@ resource "aws_lambda_function" "lambda_function_test" { `, rName) } +func testAccAWSLambdaConfigTags(rName, rSt string) string { + return fmt.Sprintf(baseAccAWSLambdaConfig(rSt)+` +resource "aws_lambda_function" "lambda_function_test" { + filename = "test-fixtures/lambdatest.zip" + function_name = "%s" + role = "${aws_iam_role.iam_for_lambda.arn}" + handler = "exports.example" + runtime = "nodejs4.3" + tags { + Key1 = "Value One" + Description = "Very interesting" + } +} +`, rName) +} + +func testAccAWSLambdaConfigTagsModified(rName, rSt string) string { + return fmt.Sprintf(baseAccAWSLambdaConfig(rSt)+` +resource "aws_lambda_function" "lambda_function_test" { + filename = "test-fixtures/lambdatest.zip" + function_name = "%s" + role = "${aws_iam_role.iam_for_lambda.arn}" + handler = "exports.example" + runtime = "nodejs4.3" + tags { + Key1 = "Value One Changed" + Key2 = "Value Two" + Key3 = "Value Three" + } +} +`, rName) +} + +func testAccAWSLambdaConfigPython36Runtime(rName, rSt string) string { + return fmt.Sprintf(baseAccAWSLambdaConfig(rSt)+` +resource "aws_lambda_function" "lambda_function_test" { + filename = "test-fixtures/lambdatest.zip" + function_name = "%s" + role = "${aws_iam_role.iam_for_lambda.arn}" + handler = "exports.example" + runtime = "python3.6" +} +`, rName) +} + const testAccAWSLambdaFunctionConfig_local_tpl = ` resource "aws_iam_role" "iam_for_lambda" { name = "iam_for_lambda_%d" diff --git a/builtin/providers/aws/tagsGeneric.go b/builtin/providers/aws/tagsGeneric.go new file mode 100644 index 000000000000..08bba6756059 --- /dev/null +++ b/builtin/providers/aws/tagsGeneric.go @@ -0,0 +1,69 @@ +package aws + +import ( + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" +) + +// diffTags takes our tags locally and the ones remotely and returns +// the set of tags that must be created, and the set of tags that must +// be destroyed. +func diffTagsGeneric(oldTags, newTags map[string]interface{}) (map[string]*string, map[string]*string) { + // First, we're creating everything we have + create := make(map[string]*string) + for k, v := range newTags { + create[k] = aws.String(v.(string)) + } + + // Build the map of what to remove + remove := make(map[string]*string) + for k, v := range oldTags { + old, ok := create[k] + if !ok || old != aws.String(v.(string)) { + // Delete it! + remove[k] = aws.String(v.(string)) + } + } + + return create, remove +} + +// tagsFromMap returns the tags for the given map of data. +func tagsFromMapGeneric(m map[string]interface{}) map[string]*string { + result := make(map[string]*string) + for k, v := range m { + if !tagIgnoredGeneric(k) { + result[k] = aws.String(v.(string)) + } + } + + return result +} + +// tagsToMap turns the tags into a map. +func tagsToMapGeneric(ts map[string]*string) map[string]string { + result := make(map[string]string) + for k, v := range ts { + if !tagIgnoredGeneric(k) { + result[k] = aws.StringValue(v) + } + } + + return result +} + +// compare a tag against a list of strings and checks if it should +// be ignored or not +func tagIgnoredGeneric(k string) bool { + filter := []string{"^aws:*"} + for _, v := range filter { + log.Printf("[DEBUG] Matching %v with %v\n", v, k) + if r, _ := regexp.MatchString(v, k); r == true { + log.Printf("[DEBUG] Found AWS specific tag %s, ignoring.\n", k) + return true + } + } + return false +} diff --git a/builtin/providers/aws/tagsGeneric_test.go b/builtin/providers/aws/tagsGeneric_test.go new file mode 100644 index 000000000000..2477f3aa5073 --- /dev/null +++ b/builtin/providers/aws/tagsGeneric_test.go @@ -0,0 +1,73 @@ +package aws + +import ( + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" +) + +// go test -v -run="TestDiffGenericTags" +func TestDiffGenericTags(t *testing.T) { + cases := []struct { + Old, New map[string]interface{} + Create, Remove map[string]string + }{ + // Basic add/remove + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "bar": "baz", + }, + Create: map[string]string{ + "bar": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + + // Modify + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "foo": "baz", + }, + Create: map[string]string{ + "foo": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + } + + for i, tc := range cases { + c, r := diffTagsGeneric(tc.Old, tc.New) + cm := tagsToMapGeneric(c) + rm := tagsToMapGeneric(r) + if !reflect.DeepEqual(cm, tc.Create) { + t.Fatalf("%d: bad create: %#v", i, cm) + } + if !reflect.DeepEqual(rm, tc.Remove) { + t.Fatalf("%d: bad remove: %#v", i, rm) + } + } +} + +// go test -v -run="TestIgnoringTagsGeneric" +func TestIgnoringTagsGeneric(t *testing.T) { + ignoredTags := map[string]*string{ + "aws:cloudformation:logical-id": aws.String("foo"), + "aws:foo:bar": aws.String("baz"), + } + for k, v := range ignoredTags { + if !tagIgnoredGeneric(k) { + t.Fatalf("Tag %v with value %v not ignored, but should be!", k, *v) + } + } +} diff --git a/builtin/providers/aws/tagsLambda.go b/builtin/providers/aws/tagsLambda.go new file mode 100644 index 000000000000..28aa25121515 --- /dev/null +++ b/builtin/providers/aws/tagsLambda.go @@ -0,0 +1,50 @@ +package aws + +import ( + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lambda" + "github.com/hashicorp/terraform/helper/schema" +) + +// setTags is a helper to set the tags for a resource. It expects the +// tags field to be named "tags" +func setTagsLambda(conn *lambda.Lambda, d *schema.ResourceData, arn string) error { + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTagsGeneric(o, n) + + // Set tags + if len(remove) > 0 { + log.Printf("[DEBUG] Removing tags: %#v", remove) + keys := make([]*string, 0, len(remove)) + for k := range remove { + keys = append(keys, aws.String(k)) + } + + _, err := conn.UntagResource(&lambda.UntagResourceInput{ + Resource: aws.String(arn), + TagKeys: keys, + }) + if err != nil { + return err + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating tags: %#v", create) + + _, err := conn.TagResource(&lambda.TagResourceInput{ + Resource: aws.String(arn), + Tags: create, + }) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/website/source/docs/providers/aws/r/lambda_function.html.markdown b/website/source/docs/providers/aws/r/lambda_function.html.markdown index 69e2003ed34a..edb0a419dc86 100644 --- a/website/source/docs/providers/aws/r/lambda_function.html.markdown +++ b/website/source/docs/providers/aws/r/lambda_function.html.markdown @@ -83,6 +83,7 @@ large files efficiently. * `environment` - (Optional) The Lambda environment's configuration settings. Fields documented below. * `kms_key_arn` - (Optional) The ARN for the KMS encryption key. * `source_code_hash` - (Optional) Used to trigger updates. Must be set to a base64-encoded SHA256 hash of the package file specified with either `filename` or `s3_key`. The usual way to set this is `${base64sha256(file("file.zip"))}`, where "file.zip" is the local filename of the lambda function source archive. +* `tags` - (Optional) A mapping of tags to assign to the object. **dead\_letter\_config** is a child block with a single argument: