terraform/builtin/providers/aws/resource_aws_lambda_functio...

490 lines
14 KiB
Go
Raw Normal View History

package aws
import (
"fmt"
"io/ioutil"
"log"
"time"
2015-06-09 21:12:47 +02:00
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/lambda"
2015-06-09 21:27:40 +02:00
"github.com/mitchellh/go-homedir"
2015-11-06 22:27:47 +01:00
"errors"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
const awsMutexLambdaKey = `aws_lambda_function`
func resourceAwsLambdaFunction() *schema.Resource {
return &schema.Resource{
Create: resourceAwsLambdaFunctionCreate,
Read: resourceAwsLambdaFunctionRead,
Update: resourceAwsLambdaFunctionUpdate,
Delete: resourceAwsLambdaFunctionDelete,
Importer: &schema.ResourceImporter{
State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
d.Set("function_name", d.Id())
return []*schema.ResourceData{d}, nil
},
},
Schema: map[string]*schema.Schema{
"filename": &schema.Schema{
2015-11-06 22:27:47 +01:00
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"s3_bucket", "s3_key", "s3_object_version"},
},
"s3_bucket": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"filename"},
},
"s3_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"filename"},
},
"s3_object_version": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"filename"},
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"function_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"handler": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"memory_size": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 128,
},
"role": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"runtime": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "nodejs",
},
"timeout": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 3,
},
"publish": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"version": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"vpc_config": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"subnet_ids": &schema.Schema{
Type: schema.TypeSet,
Required: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"security_group_ids": &schema.Schema{
Type: schema.TypeSet,
Required: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"vpc_id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
},
},
"arn": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"qualified_arn": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"last_modified": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"source_code_hash": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
},
}
}
// resourceAwsLambdaFunction maps to:
// CreateFunction in the API / SDK
func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lambdaconn
functionName := d.Get("function_name").(string)
iamRole := d.Get("role").(string)
log.Printf("[DEBUG] Creating Lambda Function %s with role %s", functionName, iamRole)
2015-11-06 22:27:47 +01:00
var functionCode *lambda.FunctionCode
if v, ok := d.GetOk("filename"); ok {
// Grab an exclusive lock so that we're only reading one function into
// memory at a time.
// See https://github.com/hashicorp/terraform/issues/9364
awsMutexKV.Lock(awsMutexLambdaKey)
defer awsMutexKV.Unlock(awsMutexLambdaKey)
file, err := loadFileContent(v.(string))
2015-11-06 22:27:47 +01:00
if err != nil {
return fmt.Errorf("Unable to load %q: %s", v.(string), err)
2015-11-06 22:27:47 +01:00
}
functionCode = &lambda.FunctionCode{
ZipFile: file,
2015-11-06 22:27:47 +01:00
}
} else {
s3Bucket, bucketOk := d.GetOk("s3_bucket")
s3Key, keyOk := d.GetOk("s3_key")
s3ObjectVersion, versionOk := d.GetOk("s3_object_version")
if !bucketOk || !keyOk {
return errors.New("s3_bucket and s3_key must all be set while using S3 code source")
2015-11-06 22:27:47 +01:00
}
functionCode = &lambda.FunctionCode{
S3Bucket: aws.String(s3Bucket.(string)),
S3Key: aws.String(s3Key.(string)),
}
if versionOk {
functionCode.S3ObjectVersion = aws.String(s3ObjectVersion.(string))
2015-11-06 22:27:47 +01:00
}
}
params := &lambda.CreateFunctionInput{
2015-11-06 22:27:47 +01:00
Code: functionCode,
Description: aws.String(d.Get("description").(string)),
FunctionName: aws.String(functionName),
Handler: aws.String(d.Get("handler").(string)),
MemorySize: aws.Int64(int64(d.Get("memory_size").(int))),
Role: aws.String(iamRole),
Runtime: aws.String(d.Get("runtime").(string)),
Timeout: aws.Int64(int64(d.Get("timeout").(int))),
Publish: aws.Bool(d.Get("publish").(bool)),
}
if v, ok := d.GetOk("vpc_config"); ok {
2016-02-18 22:45:32 +01:00
config, err := validateVPCConfig(v)
if err != nil {
return err
}
if config != nil {
var subnetIds []*string
for _, id := range config["subnet_ids"].(*schema.Set).List() {
subnetIds = append(subnetIds, aws.String(id.(string)))
}
var securityGroupIds []*string
for _, id := range config["security_group_ids"].(*schema.Set).List() {
securityGroupIds = append(securityGroupIds, aws.String(id.(string)))
}
2016-02-18 22:45:32 +01:00
params.VpcConfig = &lambda.VpcConfig{
SubnetIds: subnetIds,
SecurityGroupIds: securityGroupIds,
}
}
}
// IAM profiles can take ~10 seconds to propagate in AWS:
2016-02-18 22:45:32 +01:00
// 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.
err := resource.Retry(10*time.Minute, func() *resource.RetryError {
2016-01-05 18:35:21 +01:00
_, 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)
return resource.RetryableError(awserr)
}
}
log.Printf("[DEBUG] Error creating Lambda Function: %s", err)
return resource.NonRetryableError(err)
}
return nil
})
if err != nil {
return fmt.Errorf("Error creating Lambda function: %s", err)
}
d.SetId(d.Get("function_name").(string))
return resourceAwsLambdaFunctionRead(d, meta)
}
// resourceAwsLambdaFunctionRead maps to:
// GetFunction in the API / SDK
func resourceAwsLambdaFunctionRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lambdaconn
log.Printf("[DEBUG] Fetching Lambda Function: %s", d.Id())
params := &lambda.GetFunctionInput{
FunctionName: aws.String(d.Get("function_name").(string)),
}
getFunctionOutput, err := conn.GetFunction(params)
if err != nil {
provider/aws: Don't delete Lambda function from state on initial call of (#7829) the Read func Fixes #7782 Lambda functions are eventually consistent :( Therefore, when we move from the Create func to the Read func, there is a chance that the Lambda hasn't replicated yet and we could therefore find that it doesn't exist and delete it as follows: ``` params := &lambda.GetFunctionInput{ FunctionName: aws.String(d.Get("function_name").(string)), } getFunctionOutput, err := conn.GetFunction(params) if err != nil { if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" { d.SetId("") return nil } return err } ``` This PR uses `d.IsNewResource()` to check if the Read is being called after a Create and therefore, won't delete the lambda if not found. This should allow the lambda to replicate ``` % make testacc TEST=./builtin/providers/aws TESTARGS='-run=TestAccAWSLambdaFunction_' => Checking that code complies with gofmt requirements... go generate $(go list ./... | grep -v /terraform/vendor/) TF_ACC=1 go test ./builtin/providers/aws -v -run=TestAccAWSLambdaFunction_ -timeout 120m === RUN TestAccAWSLambdaFunction_importLocalFile --- PASS: TestAccAWSLambdaFunction_importLocalFile (36.64s) === RUN TestAccAWSLambdaFunction_importLocalFile_VPC --- PASS: TestAccAWSLambdaFunction_importLocalFile_VPC (45.17s) === RUN TestAccAWSLambdaFunction_importS3 --- PASS: TestAccAWSLambdaFunction_importS3 (40.88s) === RUN TestAccAWSLambdaFunction_basic --- PASS: TestAccAWSLambdaFunction_basic (44.77s) === RUN TestAccAWSLambdaFunction_VPC --- PASS: TestAccAWSLambdaFunction_VPC (44.13s) === RUN TestAccAWSLambdaFunction_s3 --- PASS: TestAccAWSLambdaFunction_s3 (43.62s) === RUN TestAccAWSLambdaFunction_localUpdate --- PASS: TestAccAWSLambdaFunction_localUpdate (33.49s) === RUN TestAccAWSLambdaFunction_localUpdate_nameOnly --- PASS: TestAccAWSLambdaFunction_localUpdate_nameOnly (51.83s) === RUN TestAccAWSLambdaFunction_s3Update --- PASS: TestAccAWSLambdaFunction_s3Update (106.49s) PASS ok github.com/hashicorp/terraform/builtin/providers/aws 447.055s ``` Thanks to @radeksimko for pointing out `d.IsNewResource()`
2016-07-27 17:47:25 +02:00
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" && !d.IsNewResource() {
d.SetId("")
return nil
}
return err
}
// getFunctionOutput.Code.Location is a pre-signed URL pointing at the zip
// file that we uploaded when we created the resource. You can use it to
// download the code from AWS. The other part is
// getFunctionOutput.Configuration which holds metadata.
function := getFunctionOutput.Configuration
// TODO error checking / handling on the Set() calls.
d.Set("arn", function.FunctionArn)
d.Set("description", function.Description)
d.Set("handler", function.Handler)
d.Set("memory_size", function.MemorySize)
d.Set("last_modified", function.LastModified)
d.Set("role", function.Role)
d.Set("runtime", function.Runtime)
d.Set("timeout", function.Timeout)
2016-02-18 22:45:32 +01:00
if config := flattenLambdaVpcConfigResponse(function.VpcConfig); len(config) > 0 {
log.Printf("[INFO] Setting Lambda %s VPC config %#v from API", d.Id(), config)
err := d.Set("vpc_config", config)
if err != nil {
return fmt.Errorf("Failed setting vpc_config: %s", err)
}
2016-02-18 22:45:32 +01:00
}
d.Set("source_code_hash", function.CodeSha256)
// List is sorted from oldest to latest
// so this may get costly over time :'(
var lastVersion, lastQualifiedArn string
err = listVersionsByFunctionPages(conn, &lambda.ListVersionsByFunctionInput{
FunctionName: function.FunctionName,
MaxItems: aws.Int64(10000),
}, func(p *lambda.ListVersionsByFunctionOutput, lastPage bool) bool {
if lastPage {
last := p.Versions[len(p.Versions)-1]
lastVersion = *last.Version
lastQualifiedArn = *last.FunctionArn
return true
}
return false
})
if err != nil {
return err
}
d.Set("version", lastVersion)
d.Set("qualified_arn", lastQualifiedArn)
return nil
}
func listVersionsByFunctionPages(c *lambda.Lambda, input *lambda.ListVersionsByFunctionInput,
fn func(p *lambda.ListVersionsByFunctionOutput, lastPage bool) bool) error {
for {
page, err := c.ListVersionsByFunction(input)
if err != nil {
return err
}
lastPage := page.NextMarker == nil
shouldContinue := fn(page, lastPage)
if !shouldContinue || lastPage {
break
}
}
return nil
}
// resourceAwsLambdaFunction maps to:
// DeleteFunction in the API / SDK
func resourceAwsLambdaFunctionDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lambdaconn
log.Printf("[INFO] Deleting Lambda Function: %s", d.Id())
params := &lambda.DeleteFunctionInput{
FunctionName: aws.String(d.Get("function_name").(string)),
}
_, err := conn.DeleteFunction(params)
if err != nil {
return fmt.Errorf("Error deleting Lambda Function: %s", err)
}
d.SetId("")
return nil
}
// resourceAwsLambdaFunctionUpdate maps to:
// UpdateFunctionCode in the API / SDK
func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).lambdaconn
d.Partial(true)
2016-05-27 07:38:12 +02:00
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()),
Publish: aws.Bool(d.Get("publish").(bool)),
2016-05-27 07:38:12 +02:00
}
2016-05-27 07:38:12 +02:00
if v, ok := d.GetOk("filename"); ok {
// Grab an exclusive lock so that we're only reading one function into
// memory at a time.
// See https://github.com/hashicorp/terraform/issues/9364
awsMutexKV.Lock(awsMutexLambdaKey)
defer awsMutexKV.Unlock(awsMutexLambdaKey)
file, err := loadFileContent(v.(string))
if err != nil {
return fmt.Errorf("Unable to load %q: %s", v.(string), err)
}
codeReq.ZipFile = file
2016-05-27 07:38:12 +02:00
} else {
s3Bucket, _ := d.GetOk("s3_bucket")
s3Key, _ := d.GetOk("s3_key")
s3ObjectVersion, versionOk := d.GetOk("s3_object_version")
codeReq.S3Bucket = aws.String(s3Bucket.(string))
codeReq.S3Key = aws.String(s3Key.(string))
if versionOk {
2016-05-25 10:02:20 +02:00
codeReq.S3ObjectVersion = aws.String(s3ObjectVersion.(string))
}
}
log.Printf("[DEBUG] Send Update Lambda Function Code request: %#v", codeReq)
2016-05-27 07:38:12 +02:00
_, 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
}
if configUpdate {
log.Printf("[DEBUG] Send Update Lambda Function Configuration request: %#v", configReq)
_, 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
}
2016-02-18 22:45:32 +01:00
func validateVPCConfig(v interface{}) (map[string]interface{}, error) {
configs := v.([]interface{})
if len(configs) > 1 {
return nil, errors.New("Only a single vpc_config block is expected")
}
config, ok := configs[0].(map[string]interface{})
if !ok {
return nil, errors.New("vpc_config is <nil>")
}
// if subnet_ids and security_group_ids are both empty then the VPC is optional
if config["subnet_ids"].(*schema.Set).Len() == 0 && config["security_group_ids"].(*schema.Set).Len() == 0 {
return nil, nil
}
2016-02-18 22:45:32 +01:00
if config["subnet_ids"].(*schema.Set).Len() == 0 {
return nil, errors.New("vpc_config.subnet_ids cannot be empty")
}
if config["security_group_ids"].(*schema.Set).Len() == 0 {
return nil, errors.New("vpc_config.security_group_ids cannot be empty")
}
return config, nil
}