diff --git a/builtin/providers/aws/resource_aws_lambda_function.go b/builtin/providers/aws/resource_aws_lambda_function.go index bc8517d6b632..6cd2fd062fc6 100644 --- a/builtin/providers/aws/resource_aws_lambda_function.go +++ b/builtin/providers/aws/resource_aws_lambda_function.go @@ -1,7 +1,6 @@ package aws import ( - "crypto/sha256" "fmt" "io/ioutil" "log" @@ -58,18 +57,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 +77,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, @@ -116,8 +111,8 @@ func resourceAwsLambdaFunction() *schema.Resource { }, "source_code_hash": &schema.Schema{ Type: schema.TypeString, + Optional: true, Computed: true, - ForceNew: true, }, }, } @@ -135,17 +130,12 @@ 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)) - if err != nil { - return err - } - zipfile, err := ioutil.ReadFile(filename) + 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", sha256.Sum256(zipfile)) functionCode = &lambda.FunctionCode{ - ZipFile: zipfile, + ZipFile: file, } } else { s3Bucket, bucketOk := d.GetOk("s3_bucket") @@ -202,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) @@ -256,6 +247,7 @@ func resourceAwsLambdaFunctionRead(d *schema.ResourceData, meta interface{}) err if config := flattenLambdaVpcConfigResponse(function.VpcConfig); len(config) > 0 { d.Set("vpc_config", config) } + d.Set("source_code_hash", function.CodeSha256) return nil } @@ -284,7 +276,98 @@ 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 v, ok := d.GetOk("filename"); ok && d.HasChange("source_code_hash") { + file, err := loadFileContent(v.(string)) + if err != nil { + return fmt.Errorf("Unable to load %q: %s", v.(string), err) + } + codeReq.ZipFile = file + 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) +} + +// 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 + } + fileContent, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return fileContent, nil } func validateVPCConfig(v interface{}) (map[string]interface{}, error) { diff --git a/builtin/providers/aws/resource_aws_lambda_function_test.go b/builtin/providers/aws/resource_aws_lambda_function_test.go index ac3bcd42f628..1530ec34afe0 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 = <