Merge pull request #5239 from TimeIncOSS/f-aws-lambda-func-updates

provider/aws: Add support for updating Lambda function
This commit is contained in:
Radek Simko 2016-03-11 16:59:18 +00:00
commit d8b36532ef
5 changed files with 356 additions and 15 deletions

View File

@ -1,7 +1,6 @@
package aws package aws
import ( import (
"crypto/sha256"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@ -58,18 +57,15 @@ func resourceAwsLambdaFunction() *schema.Resource {
"handler": &schema.Schema{ "handler": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, // TODO make this editable
}, },
"memory_size": &schema.Schema{ "memory_size": &schema.Schema{
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
Default: 128, Default: 128,
ForceNew: true, // TODO make this editable
}, },
"role": &schema.Schema{ "role": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true, // TODO make this editable
}, },
"runtime": &schema.Schema{ "runtime": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -81,7 +77,6 @@ func resourceAwsLambdaFunction() *schema.Resource {
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
Default: 3, Default: 3,
ForceNew: true, // TODO make this editable
}, },
"vpc_config": &schema.Schema{ "vpc_config": &schema.Schema{
Type: schema.TypeList, Type: schema.TypeList,
@ -116,8 +111,8 @@ func resourceAwsLambdaFunction() *schema.Resource {
}, },
"source_code_hash": &schema.Schema{ "source_code_hash": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true,
Computed: true, Computed: true,
ForceNew: true,
}, },
}, },
} }
@ -135,17 +130,12 @@ func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) e
var functionCode *lambda.FunctionCode var functionCode *lambda.FunctionCode
if v, ok := d.GetOk("filename"); ok { if v, ok := d.GetOk("filename"); ok {
filename, err := homedir.Expand(v.(string)) file, err := loadFileContent(v.(string))
if err != nil { if err != nil {
return err return fmt.Errorf("Unable to load %q: %s", v.(string), err)
} }
zipfile, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
d.Set("source_code_hash", sha256.Sum256(zipfile))
functionCode = &lambda.FunctionCode{ functionCode = &lambda.FunctionCode{
ZipFile: zipfile, ZipFile: file,
} }
} else { } else {
s3Bucket, bucketOk := d.GetOk("s3_bucket") 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 := resource.Retry(1*time.Minute, func() *resource.RetryError {
_, err := conn.CreateFunction(params) _, err := conn.CreateFunction(params)
if err != nil { if err != nil {
log.Printf("[ERROR] Received %q, retrying CreateFunction", err)
if awserr, ok := err.(awserr.Error); ok { if awserr, ok := err.(awserr.Error); ok {
if awserr.Code() == "InvalidParameterValueException" { if awserr.Code() == "InvalidParameterValueException" {
log.Printf("[DEBUG] InvalidParameterValueException creating Lambda Function: %s", awserr) 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 { if config := flattenLambdaVpcConfigResponse(function.VpcConfig); len(config) > 0 {
d.Set("vpc_config", config) d.Set("vpc_config", config)
} }
d.Set("source_code_hash", function.CodeSha256)
return nil return nil
} }
@ -284,7 +276,98 @@ func resourceAwsLambdaFunctionDelete(d *schema.ResourceData, meta interface{}) e
// resourceAwsLambdaFunctionUpdate maps to: // resourceAwsLambdaFunctionUpdate maps to:
// UpdateFunctionCode in the API / SDK // UpdateFunctionCode in the API / SDK
func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) error { 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) { func validateVPCConfig(v interface{}) (map[string]interface{}, error) {

View File

@ -1,7 +1,11 @@
package aws package aws
import ( import (
"archive/zip"
"fmt" "fmt"
"io/ioutil"
"os"
"path/filepath"
"strings" "strings"
"testing" "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 { func testAccCheckLambdaFunctionDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).lambdaconn 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 = ` const baseAccAWSLambdaConfig = `
resource "aws_iam_role_policy" "iam_policy_for_lambda" { resource "aws_iam_role_policy" "iam_policy_for_lambda" {
name = "iam_policy_for_lambda" name = "iam_policy_for_lambda"
@ -303,3 +457,84 @@ resource "aws_lambda_function" "lambda_function_s3test" {
handler = "exports.example" handler = "exports.example"
} }
`, acctest.RandInt()) `, acctest.RandInt())
const testAccAWSLambdaFunctionConfig_local_tpl = `
resource "aws_iam_role" "iam_for_lambda" {
name = "iam_for_lambda"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
resource "aws_lambda_function" "lambda_function_local" {
filename = "%s"
source_code_hash = "${base64sha256(file("%s"))}"
function_name = "tf_acc_lambda_name_local"
role = "${aws_iam_role.iam_for_lambda.arn}"
handler = "exports.example"
}
`
func genAWSLambdaFunctionConfig_local(filePath string) string {
return fmt.Sprintf(testAccAWSLambdaFunctionConfig_local_tpl,
filePath, filePath)
}
const testAccAWSLambdaFunctionConfig_s3_tpl = `
resource "aws_s3_bucket" "artifacts" {
bucket = "%s"
acl = "private"
force_destroy = true
versioning {
enabled = true
}
}
resource "aws_s3_bucket_object" "o" {
bucket = "${aws_s3_bucket.artifacts.bucket}"
key = "%s"
source = "%s"
etag = "${md5(file("%s"))}"
}
resource "aws_iam_role" "iam_for_lambda" {
name = "iam_for_lambda"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
resource "aws_lambda_function" "lambda_function_s3" {
s3_bucket = "${aws_s3_bucket_object.o.bucket}"
s3_key = "${aws_s3_bucket_object.o.key}"
s3_object_version = "${aws_s3_bucket_object.o.version_id}"
function_name = "tf_acc_lambda_name_s3"
role = "${aws_iam_role.iam_for_lambda.arn}"
handler = "exports.example"
}
`
func genAWSLambdaFunctionConfig_s3(bucket, key, path string) string {
return fmt.Sprintf(testAccAWSLambdaFunctionConfig_s3_tpl,
bucket, key, path, path)
}

View File

@ -0,0 +1,9 @@
var http = require('http')
exports.handler = function(event, context) {
http.get("http://requestb.in/10m32wg1", function(res) {
console.log("success", res.statusCode, res.body)
}).on('error', function(e) {
console.log("error", e)
})
}

View File

@ -0,0 +1,9 @@
var http = require('http')
exports.handler = function(event, context) {
http.get("http://requestb.in/MODIFIED", function(res) {
console.log("success", res.statusCode, res.body)
}).on('error', function(e) {
console.log("error", e)
})
}

View File

@ -39,6 +39,7 @@ resource "aws_lambda_function" "test_lambda" {
function_name = "lambda_function_name" function_name = "lambda_function_name"
role = "${aws_iam_role.iam_for_lambda.arn}" role = "${aws_iam_role.iam_for_lambda.arn}"
handler = "exports.test" 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. * `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] * `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] * `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: **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. * `arn` - The Amazon Resource Name (ARN) identifying your Lambda Function.
* `last_modified` - The date this resource was last modified. * `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 [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 [2]: https://docs.aws.amazon.com/lambda/latest/dg/walkthrough-s3-events-adminuser-create-test-function-create-function.html