Merge pull request #5239 from TimeIncOSS/f-aws-lambda-func-updates
provider/aws: Add support for updating Lambda function
This commit is contained in:
commit
d8b36532ef
|
@ -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))
|
||||
file, err := loadFileContent(v.(string))
|
||||
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{
|
||||
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) {
|
||||
|
|
|
@ -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 = <<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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue