From 69861213050332f25f1a331ea9e367f70e03aaeb Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Mon, 1 Jun 2015 09:33:22 -0700 Subject: [PATCH] AWS Lambda functionality - Includes documentation - Includes acceptance tests --- builtin/providers/aws/config.go | 5 + builtin/providers/aws/provider.go | 1 + .../aws/resource_aws_lambda_function.go | 203 ++++++++++++++++++ .../aws/resource_aws_lambda_function_test.go | 127 +++++++++++ .../aws/test-fixtures/lambdatest.zip | Bin 0 -> 342 bytes .../aws/r/lambda_function.html.markdown | 66 ++++++ website/source/layouts/aws.erb | 4 + 7 files changed, 406 insertions(+) create mode 100644 builtin/providers/aws/resource_aws_lambda_function.go create mode 100644 builtin/providers/aws/resource_aws_lambda_function_test.go create mode 100644 builtin/providers/aws/test-fixtures/lambdatest.zip create mode 100644 website/source/docs/providers/aws/r/lambda_function.html.markdown diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index 9d8397aef..685af6a06 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -15,6 +15,7 @@ import ( "github.com/awslabs/aws-sdk-go/service/elb" "github.com/awslabs/aws-sdk-go/service/iam" "github.com/awslabs/aws-sdk-go/service/kinesis" + "github.com/awslabs/aws-sdk-go/service/lambda" "github.com/awslabs/aws-sdk-go/service/rds" "github.com/awslabs/aws-sdk-go/service/route53" "github.com/awslabs/aws-sdk-go/service/s3" @@ -46,6 +47,7 @@ type AWSClient struct { iamconn *iam.IAM kinesisconn *kinesis.Kinesis elasticacheconn *elasticache.ElastiCache + lambdaconn *lambda.Lambda } // Client configures and returns a fully initailized AWSClient @@ -128,6 +130,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 9e0f928a4..2adcfb193 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -110,6 +110,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 000000000..609fbbe36 --- /dev/null +++ b/builtin/providers/aws/resource_aws_lambda_function.go @@ -0,0 +1,203 @@ +package aws + +import ( + "crypto/sha256" + "fmt" + "io/ioutil" + "log" + "strings" + "time" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/aws/awserr" + "github.com/awslabs/aws-sdk-go/service/lambda" + + "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) + + zipfile, err := ioutil.ReadFile(d.Get("filename").(string)) + 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) + } + // Add retry wrapper around this + + 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 000000000..e7b2a87ad --- /dev/null +++ b/builtin/providers/aws/resource_aws_lambda_function_test.go @@ -0,0 +1,127 @@ +package aws + +import ( + "fmt" + "testing" + "time" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/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 + time.Sleep(100 * time.Millisecond) + 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 = <k%57iL53kGF*hkCu_U#)L@%p2G=!6ZndL`H zXdn=mR&X;gvU~-q18Xlm&k&i8#+QN6XCT<*-b`*@ggZ%;WfQT_nWzO%+$*O&s=9G|AR z&f>yR^AA6>@7sK}irLaRiOHq^)$z9dpQ3v8avCCzNC$W`GRZOH@~#BX;|vTyA2BRx d1hLRO&kFH8n#TjYS=m5}8G$euNWTSf7yyUqbSVG; literal 0 HcmV?d00001 diff --git a/website/source/docs/providers/aws/r/lambda_function.html.markdown b/website/source/docs/providers/aws/r/lambda_function.html.markdown new file mode 100644 index 000000000..4c931fbad --- /dev/null +++ b/website/source/docs/providers/aws/r/lambda_function.html.markdown @@ -0,0 +1,66 @@ +--- +layout: "aws" +page_title: "AWS: aws_lambda_function" +sidebar_current: "docs-aws-resource-aws-lambda-function" +description: |- + Provides a Lambda Function resource. Lambda allows you to trigger execution of code in response to events in AWS. The Lambda Function itself includes source code and runtime configuration. +--- + +# aws\_lambda\_function + +Provides a Lambda Function resource. Lambda allows you to trigger execution of code in response to events in AWS. The Lambda Function itself includes source code and runtime configuration. + +For information about Lambda and how to use it, see [What is AWS Lambda?][1] + +## Example Usage + +``` +resource "aws_iam_role" "iam_for_lambda" { + name = "iam_for_lambda" + assume_role_policy = <> aws_vpn_gateway + + > + aws_lambda_function +