diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 6a46b5fc9dab..e3e07d659ce0 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -16,6 +16,7 @@ import ( "github.com/aws/aws-sdk-go/service/elb" "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/kinesis" + "github.com/aws/aws-sdk-go/service/lambda" "github.com/aws/aws-sdk-go/service/rds" "github.com/aws/aws-sdk-go/service/route53" "github.com/aws/aws-sdk-go/service/s3" @@ -48,6 +49,7 @@ type AWSClient struct { iamconn *iam.IAM kinesisconn *kinesis.Kinesis elasticacheconn *elasticache.ElastiCache + lambdaconn *lambda.Lambda } // Client configures and returns a fully initailized AWSClient @@ -133,6 +135,9 @@ func (c *Config) Client() (interface{}, error) { log.Println("[INFO] Initializing Elasticache Connection") client.elasticacheconn = elasticache.New(awsConfig) + + log.Println("[INFO] Initializing Lambda Connection") + client.lambdaconn = lambda.New(awsConfig) } if len(errs) > 0 { diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 3e517d71f11d..1c8ec528f800 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -115,6 +115,7 @@ func Provider() terraform.ResourceProvider { "aws_internet_gateway": resourceAwsInternetGateway(), "aws_key_pair": resourceAwsKeyPair(), "aws_kinesis_stream": resourceAwsKinesisStream(), + "aws_lambda_function": resourceAwsLambdaFunction(), "aws_launch_configuration": resourceAwsLaunchConfiguration(), "aws_lb_cookie_stickiness_policy": resourceAwsLBCookieStickinessPolicy(), "aws_main_route_table_association": resourceAwsMainRouteTableAssociation(), diff --git a/builtin/providers/aws/resource_aws_lambda_function.go b/builtin/providers/aws/resource_aws_lambda_function.go new file mode 100644 index 000000000000..72dab0154d1b --- /dev/null +++ b/builtin/providers/aws/resource_aws_lambda_function.go @@ -0,0 +1,207 @@ +package aws + +import ( + "crypto/sha256" + "fmt" + "io/ioutil" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/lambda" + "github.com/mitchellh/go-homedir" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsLambdaFunction() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsLambdaFunctionCreate, + Read: resourceAwsLambdaFunctionRead, + Update: resourceAwsLambdaFunctionUpdate, + Delete: resourceAwsLambdaFunctionDelete, + + Schema: map[string]*schema.Schema{ + "filename": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, // TODO make this editable + }, + "function_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "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, + Optional: true, + ForceNew: true, + Default: "nodejs", + }, + "timeout": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 3, + ForceNew: true, // TODO make this editable + }, + "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, + Computed: true, + ForceNew: 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) + + filename, err := homedir.Expand(d.Get("filename").(string)) + if err != nil { + return err + } + zipfile, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + d.Set("source_code_hash", sha256.Sum256(zipfile)) + + log.Printf("[DEBUG] ") + + params := &lambda.CreateFunctionInput{ + Code: &lambda.FunctionCode{ + ZipFile: zipfile, + }, + Description: aws.String(d.Get("description").(string)), + FunctionName: aws.String(functionName), + Handler: aws.String(d.Get("handler").(string)), + MemorySize: aws.Long(int64(d.Get("memory_size").(int))), + Role: aws.String(iamRole), + Runtime: aws.String(d.Get("runtime").(string)), + Timeout: aws.Long(int64(d.Get("timeout").(int))), + } + + for i := 0; i < 5; i++ { + _, err = conn.CreateFunction(params) + if awsErr, ok := err.(awserr.Error); ok { + + // IAM profiles can take ~10 seconds to propagate in AWS: + // 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. + if awsErr.Code() == "InvalidParameterValueException" && strings.Contains(awsErr.Message(), "The role defined for the task cannot be assumed by Lambda.") { + log.Printf("[DEBUG] Invalid IAM Instance Profile referenced, retrying...") + time.Sleep(2 * time.Second) + continue + } + } + break + } + 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 { + 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) + + 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 + + return nil +} diff --git a/builtin/providers/aws/resource_aws_lambda_function_test.go b/builtin/providers/aws/resource_aws_lambda_function_test.go new file mode 100644 index 000000000000..d85bd7e8d4a7 --- /dev/null +++ b/builtin/providers/aws/resource_aws_lambda_function_test.go @@ -0,0 +1,125 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/lambda" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSLambdaFunction_normal(t *testing.T) { + var conf lambda.GetFunctionOutput + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLambdaFunctionDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSLambdaConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsLambdaFunctionExists("aws_lambda_function.lambda_function_test", &conf), + testAccCheckAWSLambdaAttributes(&conf), + ), + }, + }, + }) +} + +func testAccCheckLambdaFunctionDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).lambdaconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_lambda_function" { + continue + } + + _, err := conn.GetFunction(&lambda.GetFunctionInput{ + FunctionName: aws.String(rs.Primary.ID), + }) + + if err == nil { + return fmt.Errorf("Lambda Function still exists") + } + + } + + return nil + +} + +func testAccCheckAwsLambdaFunctionExists(n string, function *lambda.GetFunctionOutput) resource.TestCheckFunc { + // Wait for IAM role + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Lambda function not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("Lambda function ID not set") + } + + conn := testAccProvider.Meta().(*AWSClient).lambdaconn + + params := &lambda.GetFunctionInput{ + FunctionName: aws.String("example_lambda_name"), + } + + getFunction, err := conn.GetFunction(params) + if err != nil { + return err + } + + *function = *getFunction + + return nil + } +} + +func testAccCheckAWSLambdaAttributes(function *lambda.GetFunctionOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + c := function.Configuration + const expectedName = "example_lambda_name" + if *c.FunctionName != expectedName { + return fmt.Errorf("Expected function name %s, got %s", expectedName, *c.FunctionName) + } + + if *c.FunctionARN == "" { + return fmt.Errorf("Could not read Lambda Function's ARN") + } + + return nil + } +} + +const testAccAWSLambdaConfig = ` +resource "aws_iam_role" "iam_for_lambda" { + name = "iam_for_lambda" + assume_role_policy = <> aws_vpn_gateway + + > + aws_lambda_function +