From 4aeb5bb8e961bfb82e1fae3c3106b6186ae1c3de Mon Sep 17 00:00:00 2001 From: Rob Zienert Date: Mon, 9 Nov 2015 11:39:24 -0600 Subject: [PATCH 1/5] Adding support for Lambda function updates --- .../aws/resource_aws_lambda_function.go | 109 ++++++++++++++++-- 1 file changed, 98 insertions(+), 11 deletions(-) diff --git a/builtin/providers/aws/resource_aws_lambda_function.go b/builtin/providers/aws/resource_aws_lambda_function.go index bc8517d6b..8da45720e 100644 --- a/builtin/providers/aws/resource_aws_lambda_function.go +++ b/builtin/providers/aws/resource_aws_lambda_function.go @@ -58,18 +58,15 @@ func resourceAwsLambdaFunction() *schema.Resource { "handler": &schema.Schema{ Type: schema.TypeString, Required: true, - ForceNew: true, // TODO make this editable }, "memory_size": &schema.Schema{ Type: schema.TypeInt, Optional: true, Default: 128, - ForceNew: true, // TODO make this editable }, "role": &schema.Schema{ Type: schema.TypeString, Required: true, - ForceNew: true, // TODO make this editable }, "runtime": &schema.Schema{ Type: schema.TypeString, @@ -81,7 +78,6 @@ func resourceAwsLambdaFunction() *schema.Resource { Type: schema.TypeInt, Optional: true, Default: 3, - ForceNew: true, // TODO make this editable }, "vpc_config": &schema.Schema{ Type: schema.TypeList, @@ -135,15 +131,11 @@ func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) e var functionCode *lambda.FunctionCode if v, ok := d.GetOk("filename"); ok { - filename, err := homedir.Expand(v.(string)) + zipfile, shaSum, err := loadLocalZipFile(v.(string)) if err != nil { return err } - zipfile, err := ioutil.ReadFile(filename) - if err != nil { - return err - } - d.Set("source_code_hash", sha256.Sum256(zipfile)) + d.Set("source_code_hash", shaSum) functionCode = &lambda.FunctionCode{ ZipFile: zipfile, } @@ -284,7 +276,102 @@ func resourceAwsLambdaFunctionDelete(d *schema.ResourceData, meta interface{}) e // resourceAwsLambdaFunctionUpdate maps to: // UpdateFunctionCode in the API / SDK func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) error { - return nil + conn := meta.(*AWSClient).lambdaconn + + d.Partial(true) + + codeReq := &lambda.UpdateFunctionCodeInput{ + FunctionName: aws.String(d.Id()), + } + + codeUpdate := false + if sourceHash, ok := d.GetOk("source_code_hash"); ok { + zipfile, shaSum, err := loadLocalZipFile(d.Get("filename").(string)) + if err != nil { + return err + } + if sourceHash != shaSum { + d.SetPartial("filename") + d.SetPartial("source_code_hash") + } + codeReq.ZipFile = zipfile + codeUpdate = true + } + if d.HasChange("s3_bucket") || d.HasChange("s3_key") || d.HasChange("s3_object_version") { + codeReq.S3Bucket = aws.String(d.Get("s3_bucket").(string)) + codeReq.S3Key = aws.String(d.Get("s3_key").(string)) + codeReq.S3ObjectVersion = aws.String(d.Get("s3_object_version").(string)) + codeUpdate = true + } + + log.Printf("[DEBUG] Send Update Lambda Function Code request: %#v", codeReq) + if codeUpdate { + _, err := conn.UpdateFunctionCode(codeReq) + if err != nil { + return fmt.Errorf("Error modifying Lambda Function Code %s: %s", d.Id(), err) + } + + d.SetPartial("filename") + d.SetPartial("source_code_hash") + d.SetPartial("s3_bucket") + d.SetPartial("s3_key") + d.SetPartial("s3_object_version") + } + + configReq := &lambda.UpdateFunctionConfigurationInput{ + FunctionName: aws.String(d.Id()), + } + + configUpdate := false + if d.HasChange("description") { + configReq.Description = aws.String(d.Get("description").(string)) + configUpdate = true + } + if d.HasChange("handler") { + configReq.Handler = aws.String(d.Get("handler").(string)) + configUpdate = true + } + if d.HasChange("memory_size") { + configReq.MemorySize = aws.Int64(int64(d.Get("memory_size").(int))) + configUpdate = true + } + if d.HasChange("role") { + configReq.Role = aws.String(d.Get("role").(string)) + configUpdate = true + } + if d.HasChange("timeout") { + configReq.Timeout = aws.Int64(int64(d.Get("timeout").(int))) + configUpdate = true + } + + log.Printf("[DEBUG] Send Update Lambda Function Configuration request: %#v", configReq) + if configUpdate { + _, err := conn.UpdateFunctionConfiguration(configReq) + if err != nil { + return fmt.Errorf("Error modifying Lambda Function Configuration %s: %s", d.Id(), err) + } + d.SetPartial("description") + d.SetPartial("handler") + d.SetPartial("memory_size") + d.SetPartial("role") + d.SetPartial("timeout") + } + d.Partial(false) + + return resourceAwsLambdaFunctionRead(d, meta) +} + +func loadLocalZipFile(v string) ([]byte, string, error) { + filename, err := homedir.Expand(v) + if err != nil { + return nil, "", err + } + zipfile, err := ioutil.ReadFile(filename) + if err != nil { + return nil, "", err + } + sum := sha256.Sum256(zipfile) + return zipfile, string(sum[:32]), nil } func validateVPCConfig(v interface{}) (map[string]interface{}, error) { From b5c7521f52b72493eceeb3b3be2024c559b74511 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Wed, 9 Dec 2015 17:42:12 -0800 Subject: [PATCH 2/5] aws_lambda_function: Force code update on SHA sum mismatch --- .../aws/resource_aws_lambda_function.go | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/builtin/providers/aws/resource_aws_lambda_function.go b/builtin/providers/aws/resource_aws_lambda_function.go index 8da45720e..178b0d88f 100644 --- a/builtin/providers/aws/resource_aws_lambda_function.go +++ b/builtin/providers/aws/resource_aws_lambda_function.go @@ -2,6 +2,7 @@ package aws import ( "crypto/sha256" + "encoding/base64" "fmt" "io/ioutil" "log" @@ -102,6 +103,11 @@ func resourceAwsLambdaFunction() *schema.Resource { }, }, }, + "update_code": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, "arn": &schema.Schema{ Type: schema.TypeString, Computed: true, @@ -113,7 +119,10 @@ func resourceAwsLambdaFunction() *schema.Resource { "source_code_hash": &schema.Schema{ Type: schema.TypeString, Computed: true, - ForceNew: true, + }, + "remote_code_hash": &schema.Schema{ + Type: schema.TypeString, + Computed: true, }, }, } @@ -249,6 +258,26 @@ func resourceAwsLambdaFunctionRead(d *schema.ResourceData, meta interface{}) err d.Set("vpc_config", config) } + // Compare code hashes, and see if an update is required to code. If there + // is, set the "update_code" attribute. + + remoteSum, err := decodeBase64(*function.CodeSha256) + if err != nil { + return err + } + _, localSum, err := loadLocalZipFile(d.Get("filename").(string)) + if err != nil { + return err + } + d.Set("remote_code_hash", remoteSum) + d.Set("source_code_hash", localSum) + + if remoteSum != localSum { + d.Set("update_code", true) + } else { + d.Set("update_code", false) + } + return nil } @@ -361,6 +390,7 @@ func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) e return resourceAwsLambdaFunctionRead(d, meta) } +// loads the local ZIP data and the SHA sum of the data. func loadLocalZipFile(v string) ([]byte, string, error) { filename, err := homedir.Expand(v) if err != nil { @@ -371,7 +401,16 @@ func loadLocalZipFile(v string) ([]byte, string, error) { return nil, "", err } sum := sha256.Sum256(zipfile) - return zipfile, string(sum[:32]), nil + return zipfile, fmt.Sprintf("%x", sum), nil +} + +// Decodes a base64 string to a string. +func decodeBase64(s string) (string, error) { + sum, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", sum), nil } func validateVPCConfig(v interface{}) (map[string]interface{}, error) { From fdc21aad25b947a443126ed8d0d39c1e0ff263a9 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 19 Feb 2016 12:13:26 +0000 Subject: [PATCH 3/5] provider/aws: Simplify update logic for Lambda function --- .../aws/resource_aws_lambda_function.go | 74 ++++--------------- 1 file changed, 15 insertions(+), 59 deletions(-) diff --git a/builtin/providers/aws/resource_aws_lambda_function.go b/builtin/providers/aws/resource_aws_lambda_function.go index 178b0d88f..692d1b0f3 100644 --- a/builtin/providers/aws/resource_aws_lambda_function.go +++ b/builtin/providers/aws/resource_aws_lambda_function.go @@ -1,8 +1,6 @@ package aws import ( - "crypto/sha256" - "encoding/base64" "fmt" "io/ioutil" "log" @@ -103,11 +101,6 @@ func resourceAwsLambdaFunction() *schema.Resource { }, }, }, - "update_code": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: false, - }, "arn": &schema.Schema{ Type: schema.TypeString, Computed: true, @@ -118,10 +111,7 @@ func resourceAwsLambdaFunction() *schema.Resource { }, "source_code_hash": &schema.Schema{ Type: schema.TypeString, - Computed: true, - }, - "remote_code_hash": &schema.Schema{ - Type: schema.TypeString, + Optional: true, Computed: true, }, }, @@ -140,13 +130,12 @@ func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) e var functionCode *lambda.FunctionCode if v, ok := d.GetOk("filename"); ok { - zipfile, shaSum, err := loadLocalZipFile(v.(string)) + file, err := loadFileContent(v.(string)) if err != nil { - return err + return fmt.Errorf("Unable to load %q: %s", v.(string), err) } - d.Set("source_code_hash", shaSum) functionCode = &lambda.FunctionCode{ - ZipFile: zipfile, + ZipFile: file, } } else { s3Bucket, bucketOk := d.GetOk("s3_bucket") @@ -257,26 +246,7 @@ func resourceAwsLambdaFunctionRead(d *schema.ResourceData, meta interface{}) err if config := flattenLambdaVpcConfigResponse(function.VpcConfig); len(config) > 0 { d.Set("vpc_config", config) } - - // Compare code hashes, and see if an update is required to code. If there - // is, set the "update_code" attribute. - - remoteSum, err := decodeBase64(*function.CodeSha256) - if err != nil { - return err - } - _, localSum, err := loadLocalZipFile(d.Get("filename").(string)) - if err != nil { - return err - } - d.Set("remote_code_hash", remoteSum) - d.Set("source_code_hash", localSum) - - if remoteSum != localSum { - d.Set("update_code", true) - } else { - d.Set("update_code", false) - } + d.Set("source_code_hash", function.CodeSha256) return nil } @@ -314,16 +284,12 @@ func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) e } codeUpdate := false - if sourceHash, ok := d.GetOk("source_code_hash"); ok { - zipfile, shaSum, err := loadLocalZipFile(d.Get("filename").(string)) + if v, ok := d.GetOk("filename"); ok && d.HasChange("source_code_hash") { + file, err := loadFileContent(v.(string)) if err != nil { - return err + return fmt.Errorf("Unable to load %q: %s", v.(string), err) } - if sourceHash != shaSum { - d.SetPartial("filename") - d.SetPartial("source_code_hash") - } - codeReq.ZipFile = zipfile + codeReq.ZipFile = file codeUpdate = true } if d.HasChange("s3_bucket") || d.HasChange("s3_key") || d.HasChange("s3_object_version") { @@ -390,27 +356,17 @@ func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) e return resourceAwsLambdaFunctionRead(d, meta) } -// loads the local ZIP data and the SHA sum of the data. -func loadLocalZipFile(v string) ([]byte, string, error) { +// loadFileContent returns contents of a file in a given path +func loadFileContent(v string) ([]byte, error) { filename, err := homedir.Expand(v) if err != nil { - return nil, "", err + return nil, err } - zipfile, err := ioutil.ReadFile(filename) + fileContent, err := ioutil.ReadFile(filename) if err != nil { - return nil, "", err + return nil, err } - sum := sha256.Sum256(zipfile) - return zipfile, fmt.Sprintf("%x", sum), nil -} - -// Decodes a base64 string to a string. -func decodeBase64(s string) (string, error) { - sum, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return "", err - } - return fmt.Sprintf("%x", sum), nil + return fileContent, nil } func validateVPCConfig(v interface{}) (map[string]interface{}, error) { From d777141a7b03ad135bd0e0485097f9ddccb8aefc Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Sun, 21 Feb 2016 20:54:30 +0000 Subject: [PATCH 4/5] provider/aws: Add tests for Lambda function updates --- .../aws/resource_aws_lambda_function.go | 1 + .../aws/resource_aws_lambda_function_test.go | 235 ++++++++++++++++++ .../aws/test-fixtures/lambda_func.js | 9 + .../aws/test-fixtures/lambda_func_modified.js | 9 + 4 files changed, 254 insertions(+) create mode 100644 builtin/providers/aws/test-fixtures/lambda_func.js create mode 100644 builtin/providers/aws/test-fixtures/lambda_func_modified.js diff --git a/builtin/providers/aws/resource_aws_lambda_function.go b/builtin/providers/aws/resource_aws_lambda_function.go index 692d1b0f3..6cd2fd062 100644 --- a/builtin/providers/aws/resource_aws_lambda_function.go +++ b/builtin/providers/aws/resource_aws_lambda_function.go @@ -192,6 +192,7 @@ func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) e err := resource.Retry(1*time.Minute, func() *resource.RetryError { _, err := conn.CreateFunction(params) if err != nil { + log.Printf("[ERROR] Received %q, retrying CreateFunction", err) if awserr, ok := err.(awserr.Error); ok { if awserr.Code() == "InvalidParameterValueException" { log.Printf("[DEBUG] InvalidParameterValueException creating Lambda Function: %s", awserr) diff --git a/builtin/providers/aws/resource_aws_lambda_function_test.go b/builtin/providers/aws/resource_aws_lambda_function_test.go index ac3bcd42f..1530ec34a 100644 --- a/builtin/providers/aws/resource_aws_lambda_function_test.go +++ b/builtin/providers/aws/resource_aws_lambda_function_test.go @@ -1,7 +1,11 @@ package aws import ( + "archive/zip" "fmt" + "io/ioutil" + "os" + "path/filepath" "strings" "testing" @@ -74,6 +78,101 @@ func TestAccAWSLambdaFunction_s3(t *testing.T) { }) } +func TestAccAWSLambdaFunction_localUpdate(t *testing.T) { + var conf lambda.GetFunctionOutput + + path, zipFile, err := createTempFile("lambda_localUpdate") + if err != nil { + t.Fatal(err) + } + defer os.Remove(path) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLambdaFunctionDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + PreConfig: func() { + testAccCreateZipFromFiles(map[string]string{"test-fixtures/lambda_func.js": "lambda.js"}, zipFile) + }, + Config: genAWSLambdaFunctionConfig_local(path), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_local", "tf_acc_lambda_name_local", &conf), + testAccCheckAwsLambdaFunctionName(&conf, "tf_acc_lambda_name_local"), + testAccCheckAwsLambdaFunctionArnHasSuffix(&conf, "tf_acc_lambda_name_local"), + testAccCheckAwsLambdaSourceCodeHash(&conf, "un6qF9S9hKvXbWwJ6m2EYaVCWjcr0PCZWiTV3h4zB0I="), + ), + }, + resource.TestStep{ + PreConfig: func() { + testAccCreateZipFromFiles(map[string]string{"test-fixtures/lambda_func_modified.js": "lambda.js"}, zipFile) + }, + Config: genAWSLambdaFunctionConfig_local(path), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_local", "tf_acc_lambda_name_local", &conf), + testAccCheckAwsLambdaFunctionName(&conf, "tf_acc_lambda_name_local"), + testAccCheckAwsLambdaFunctionArnHasSuffix(&conf, "tf_acc_lambda_name_local"), + testAccCheckAwsLambdaSourceCodeHash(&conf, "Y5Jf4Si63UDy1wKNfPs+U56ZL0NxsieKPt9EwRl4GQM="), + ), + }, + }, + }) +} + +func TestAccAWSLambdaFunction_s3Update(t *testing.T) { + var conf lambda.GetFunctionOutput + + path, zipFile, err := createTempFile("lambda_s3Update") + if err != nil { + t.Fatal(err) + } + defer os.Remove(path) + + bucketName := fmt.Sprintf("tf-acc-lambda-s3-deployments-%d", randomInteger) + key := "lambda-func.zip" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLambdaFunctionDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + PreConfig: func() { + // Upload 1st version + testAccCreateZipFromFiles(map[string]string{"test-fixtures/lambda_func.js": "lambda.js"}, zipFile) + }, + Config: genAWSLambdaFunctionConfig_s3(bucketName, key, path), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_s3", "tf_acc_lambda_name_s3", &conf), + testAccCheckAwsLambdaFunctionName(&conf, "tf_acc_lambda_name_s3"), + testAccCheckAwsLambdaFunctionArnHasSuffix(&conf, "tf_acc_lambda_name_s3"), + testAccCheckAwsLambdaSourceCodeHash(&conf, "un6qF9S9hKvXbWwJ6m2EYaVCWjcr0PCZWiTV3h4zB0I="), + ), + }, + resource.TestStep{ + ExpectNonEmptyPlan: true, + PreConfig: func() { + // Upload 2nd version + testAccCreateZipFromFiles(map[string]string{"test-fixtures/lambda_func_modified.js": "lambda.js"}, zipFile) + }, + Config: genAWSLambdaFunctionConfig_s3(bucketName, key, path), + }, + // Extra step because of missing ComputedWhen + // See https://github.com/hashicorp/terraform/pull/4846 & https://github.com/hashicorp/terraform/pull/5330 + resource.TestStep{ + Config: genAWSLambdaFunctionConfig_s3(bucketName, key, path), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_s3", "tf_acc_lambda_name_s3", &conf), + testAccCheckAwsLambdaFunctionName(&conf, "tf_acc_lambda_name_s3"), + testAccCheckAwsLambdaFunctionArnHasSuffix(&conf, "tf_acc_lambda_name_s3"), + testAccCheckAwsLambdaSourceCodeHash(&conf, "Y5Jf4Si63UDy1wKNfPs+U56ZL0NxsieKPt9EwRl4GQM="), + ), + }, + }, + }) +} + func testAccCheckLambdaFunctionDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).lambdaconn @@ -157,6 +256,61 @@ func testAccCheckAwsLambdaFunctionArnHasSuffix(function *lambda.GetFunctionOutpu } } +func testAccCheckAwsLambdaSourceCodeHash(function *lambda.GetFunctionOutput, expectedHash string) resource.TestCheckFunc { + return func(s *terraform.State) error { + c := function.Configuration + if *c.CodeSha256 != expectedHash { + return fmt.Errorf("Expected code hash %s, got %s", expectedHash, *c.CodeSha256) + } + + return nil + } +} + +func testAccCreateZipFromFiles(files map[string]string, zipFile *os.File) error { + zipFile.Truncate(0) + zipFile.Seek(0, 0) + + w := zip.NewWriter(zipFile) + + for source, destination := range files { + f, err := w.Create(destination) + if err != nil { + return err + } + + fileContent, err := ioutil.ReadFile(source) + if err != nil { + return err + } + + _, err = f.Write(fileContent) + if err != nil { + return err + } + } + + err := w.Close() + if err != nil { + return err + } + + return w.Flush() +} + +func createTempFile(prefix string) (string, *os.File, error) { + f, err := ioutil.TempFile(os.TempDir(), prefix) + if err != nil { + return "", nil, err + } + + pathToFile, err := filepath.Abs(f.Name()) + if err != nil { + return "", nil, err + } + return pathToFile, f, nil +} + const baseAccAWSLambdaConfig = ` resource "aws_iam_role_policy" "iam_policy_for_lambda" { name = "iam_policy_for_lambda" @@ -303,3 +457,84 @@ resource "aws_lambda_function" "lambda_function_s3test" { handler = "exports.example" } `, acctest.RandInt()) + +const testAccAWSLambdaFunctionConfig_local_tpl = ` +resource "aws_iam_role" "iam_for_lambda" { + name = "iam_for_lambda" + assume_role_policy = < Date: Thu, 25 Feb 2016 19:41:35 +0000 Subject: [PATCH 5/5] provider/aws: Document lambda_function updates --- .../docs/providers/aws/r/lambda_function.html.markdown | 5 +++++ 1 file changed, 5 insertions(+) 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 7abd33ef6..41857cda7 100644 --- a/website/source/docs/providers/aws/r/lambda_function.html.markdown +++ b/website/source/docs/providers/aws/r/lambda_function.html.markdown @@ -39,6 +39,7 @@ resource "aws_lambda_function" "test_lambda" { function_name = "lambda_function_name" role = "${aws_iam_role.iam_for_lambda.arn}" handler = "exports.test" + source_code_hash = "${base64sha256(file("lambda_function_payload.zip"))}" } ``` @@ -56,6 +57,8 @@ resource "aws_lambda_function" "test_lambda" { * `runtime` - (Optional) Defaults to `nodejs`. See [Runtimes][6] for valid values. * `timeout` - (Optional) The amount of time your Lambda Function has to run in seconds. Defaults to `3`. See [Limits][5] * `vpc_config` - (Optional) Provide this to allow your function to access your VPC. Fields documented below. See [Lambda in VPC][7] +* `source_code_hash` - (Optional) Used to trigger updates. This is only useful in conjuction with `filename`. + The only useful value is `${base64sha256(file("file.zip"))}`. **vpc\_config** requires the following: @@ -66,6 +69,8 @@ resource "aws_lambda_function" "test_lambda" { * `arn` - The Amazon Resource Name (ARN) identifying your Lambda Function. * `last_modified` - The date this resource was last modified. +* `source_code_hash` - Base64-encoded representation of raw SHA-256 sum of the zip file + provided either via `filename` or `s3_*` parameters [1]: https://docs.aws.amazon.com/lambda/latest/dg/welcome.html [2]: https://docs.aws.amazon.com/lambda/latest/dg/walkthrough-s3-events-adminuser-create-test-function-create-function.html