github.com/emc-cmd/terraform@v0.7.8-0.20161101145618-f16309630e7c/builtin/providers/aws/resource_aws_lambda_function.go (about)

     1  package aws
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"log"
     7  	"time"
     8  
     9  	"github.com/aws/aws-sdk-go/aws"
    10  	"github.com/aws/aws-sdk-go/aws/awserr"
    11  	"github.com/aws/aws-sdk-go/service/lambda"
    12  	"github.com/mitchellh/go-homedir"
    13  
    14  	"errors"
    15  
    16  	"github.com/hashicorp/terraform/helper/resource"
    17  	"github.com/hashicorp/terraform/helper/schema"
    18  )
    19  
    20  const awsMutexLambdaKey = `aws_lambda_function`
    21  
    22  func resourceAwsLambdaFunction() *schema.Resource {
    23  	return &schema.Resource{
    24  		Create: resourceAwsLambdaFunctionCreate,
    25  		Read:   resourceAwsLambdaFunctionRead,
    26  		Update: resourceAwsLambdaFunctionUpdate,
    27  		Delete: resourceAwsLambdaFunctionDelete,
    28  
    29  		Importer: &schema.ResourceImporter{
    30  			State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
    31  				d.Set("function_name", d.Id())
    32  				return []*schema.ResourceData{d}, nil
    33  			},
    34  		},
    35  
    36  		Schema: map[string]*schema.Schema{
    37  			"filename": &schema.Schema{
    38  				Type:          schema.TypeString,
    39  				Optional:      true,
    40  				ConflictsWith: []string{"s3_bucket", "s3_key", "s3_object_version"},
    41  			},
    42  			"s3_bucket": &schema.Schema{
    43  				Type:          schema.TypeString,
    44  				Optional:      true,
    45  				ConflictsWith: []string{"filename"},
    46  			},
    47  			"s3_key": &schema.Schema{
    48  				Type:          schema.TypeString,
    49  				Optional:      true,
    50  				ConflictsWith: []string{"filename"},
    51  			},
    52  			"s3_object_version": &schema.Schema{
    53  				Type:          schema.TypeString,
    54  				Optional:      true,
    55  				ConflictsWith: []string{"filename"},
    56  			},
    57  			"description": &schema.Schema{
    58  				Type:     schema.TypeString,
    59  				Optional: true,
    60  			},
    61  			"function_name": &schema.Schema{
    62  				Type:     schema.TypeString,
    63  				Required: true,
    64  				ForceNew: true,
    65  			},
    66  			"handler": &schema.Schema{
    67  				Type:     schema.TypeString,
    68  				Required: true,
    69  			},
    70  			"memory_size": &schema.Schema{
    71  				Type:     schema.TypeInt,
    72  				Optional: true,
    73  				Default:  128,
    74  			},
    75  			"role": &schema.Schema{
    76  				Type:     schema.TypeString,
    77  				Required: true,
    78  			},
    79  			"runtime": &schema.Schema{
    80  				Type:     schema.TypeString,
    81  				Optional: true,
    82  				ForceNew: true,
    83  				Default:  "nodejs",
    84  			},
    85  			"timeout": &schema.Schema{
    86  				Type:     schema.TypeInt,
    87  				Optional: true,
    88  				Default:  3,
    89  			},
    90  			"publish": &schema.Schema{
    91  				Type:     schema.TypeBool,
    92  				Optional: true,
    93  				Default:  false,
    94  			},
    95  			"version": &schema.Schema{
    96  				Type:     schema.TypeString,
    97  				Computed: true,
    98  			},
    99  			"vpc_config": &schema.Schema{
   100  				Type:     schema.TypeList,
   101  				Optional: true,
   102  				ForceNew: true,
   103  				Elem: &schema.Resource{
   104  					Schema: map[string]*schema.Schema{
   105  						"subnet_ids": &schema.Schema{
   106  							Type:     schema.TypeSet,
   107  							Required: true,
   108  							ForceNew: true,
   109  							Elem:     &schema.Schema{Type: schema.TypeString},
   110  							Set:      schema.HashString,
   111  						},
   112  						"security_group_ids": &schema.Schema{
   113  							Type:     schema.TypeSet,
   114  							Required: true,
   115  							ForceNew: true,
   116  							Elem:     &schema.Schema{Type: schema.TypeString},
   117  							Set:      schema.HashString,
   118  						},
   119  						"vpc_id": &schema.Schema{
   120  							Type:     schema.TypeString,
   121  							Computed: true,
   122  						},
   123  					},
   124  				},
   125  			},
   126  			"arn": &schema.Schema{
   127  				Type:     schema.TypeString,
   128  				Computed: true,
   129  			},
   130  			"qualified_arn": &schema.Schema{
   131  				Type:     schema.TypeString,
   132  				Computed: true,
   133  			},
   134  			"last_modified": &schema.Schema{
   135  				Type:     schema.TypeString,
   136  				Computed: true,
   137  			},
   138  			"source_code_hash": &schema.Schema{
   139  				Type:     schema.TypeString,
   140  				Optional: true,
   141  				Computed: true,
   142  			},
   143  		},
   144  	}
   145  }
   146  
   147  // resourceAwsLambdaFunction maps to:
   148  // CreateFunction in the API / SDK
   149  func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) error {
   150  	conn := meta.(*AWSClient).lambdaconn
   151  
   152  	functionName := d.Get("function_name").(string)
   153  	iamRole := d.Get("role").(string)
   154  
   155  	log.Printf("[DEBUG] Creating Lambda Function %s with role %s", functionName, iamRole)
   156  
   157  	var functionCode *lambda.FunctionCode
   158  	if v, ok := d.GetOk("filename"); ok {
   159  		// Grab an exclusive lock so that we're only reading one function into
   160  		// memory at a time.
   161  		// See https://github.com/hashicorp/terraform/issues/9364
   162  		awsMutexKV.Lock(awsMutexLambdaKey)
   163  		defer awsMutexKV.Unlock(awsMutexLambdaKey)
   164  		file, err := loadFileContent(v.(string))
   165  		if err != nil {
   166  			return fmt.Errorf("Unable to load %q: %s", v.(string), err)
   167  		}
   168  		functionCode = &lambda.FunctionCode{
   169  			ZipFile: file,
   170  		}
   171  	} else {
   172  		s3Bucket, bucketOk := d.GetOk("s3_bucket")
   173  		s3Key, keyOk := d.GetOk("s3_key")
   174  		s3ObjectVersion, versionOk := d.GetOk("s3_object_version")
   175  		if !bucketOk || !keyOk {
   176  			return errors.New("s3_bucket and s3_key must all be set while using S3 code source")
   177  		}
   178  		functionCode = &lambda.FunctionCode{
   179  			S3Bucket: aws.String(s3Bucket.(string)),
   180  			S3Key:    aws.String(s3Key.(string)),
   181  		}
   182  		if versionOk {
   183  			functionCode.S3ObjectVersion = aws.String(s3ObjectVersion.(string))
   184  		}
   185  	}
   186  
   187  	params := &lambda.CreateFunctionInput{
   188  		Code:         functionCode,
   189  		Description:  aws.String(d.Get("description").(string)),
   190  		FunctionName: aws.String(functionName),
   191  		Handler:      aws.String(d.Get("handler").(string)),
   192  		MemorySize:   aws.Int64(int64(d.Get("memory_size").(int))),
   193  		Role:         aws.String(iamRole),
   194  		Runtime:      aws.String(d.Get("runtime").(string)),
   195  		Timeout:      aws.Int64(int64(d.Get("timeout").(int))),
   196  		Publish:      aws.Bool(d.Get("publish").(bool)),
   197  	}
   198  
   199  	if v, ok := d.GetOk("vpc_config"); ok {
   200  		config, err := validateVPCConfig(v)
   201  		if err != nil {
   202  			return err
   203  		}
   204  
   205  		if config != nil {
   206  			var subnetIds []*string
   207  			for _, id := range config["subnet_ids"].(*schema.Set).List() {
   208  				subnetIds = append(subnetIds, aws.String(id.(string)))
   209  			}
   210  
   211  			var securityGroupIds []*string
   212  			for _, id := range config["security_group_ids"].(*schema.Set).List() {
   213  				securityGroupIds = append(securityGroupIds, aws.String(id.(string)))
   214  			}
   215  
   216  			params.VpcConfig = &lambda.VpcConfig{
   217  				SubnetIds:        subnetIds,
   218  				SecurityGroupIds: securityGroupIds,
   219  			}
   220  		}
   221  	}
   222  
   223  	// IAM profiles can take ~10 seconds to propagate in AWS:
   224  	// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console
   225  	// Error creating Lambda function: InvalidParameterValueException: The role defined for the task cannot be assumed by Lambda.
   226  	err := resource.Retry(1*time.Minute, func() *resource.RetryError {
   227  		_, err := conn.CreateFunction(params)
   228  		if err != nil {
   229  			log.Printf("[ERROR] Received %q, retrying CreateFunction", err)
   230  			if awserr, ok := err.(awserr.Error); ok {
   231  				if awserr.Code() == "InvalidParameterValueException" {
   232  					log.Printf("[DEBUG] InvalidParameterValueException creating Lambda Function: %s", awserr)
   233  					return resource.RetryableError(awserr)
   234  				}
   235  			}
   236  			log.Printf("[DEBUG] Error creating Lambda Function: %s", err)
   237  			return resource.NonRetryableError(err)
   238  		}
   239  		return nil
   240  	})
   241  	if err != nil {
   242  		return fmt.Errorf("Error creating Lambda function: %s", err)
   243  	}
   244  
   245  	d.SetId(d.Get("function_name").(string))
   246  
   247  	return resourceAwsLambdaFunctionRead(d, meta)
   248  }
   249  
   250  // resourceAwsLambdaFunctionRead maps to:
   251  // GetFunction in the API / SDK
   252  func resourceAwsLambdaFunctionRead(d *schema.ResourceData, meta interface{}) error {
   253  	conn := meta.(*AWSClient).lambdaconn
   254  
   255  	log.Printf("[DEBUG] Fetching Lambda Function: %s", d.Id())
   256  
   257  	params := &lambda.GetFunctionInput{
   258  		FunctionName: aws.String(d.Get("function_name").(string)),
   259  	}
   260  
   261  	getFunctionOutput, err := conn.GetFunction(params)
   262  	if err != nil {
   263  		if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ResourceNotFoundException" && !d.IsNewResource() {
   264  			d.SetId("")
   265  			return nil
   266  		}
   267  		return err
   268  	}
   269  
   270  	// getFunctionOutput.Code.Location is a pre-signed URL pointing at the zip
   271  	// file that we uploaded when we created the resource. You can use it to
   272  	// download the code from AWS. The other part is
   273  	// getFunctionOutput.Configuration which holds metadata.
   274  
   275  	function := getFunctionOutput.Configuration
   276  	// TODO error checking / handling on the Set() calls.
   277  	d.Set("arn", function.FunctionArn)
   278  	d.Set("description", function.Description)
   279  	d.Set("handler", function.Handler)
   280  	d.Set("memory_size", function.MemorySize)
   281  	d.Set("last_modified", function.LastModified)
   282  	d.Set("role", function.Role)
   283  	d.Set("runtime", function.Runtime)
   284  	d.Set("timeout", function.Timeout)
   285  	if config := flattenLambdaVpcConfigResponse(function.VpcConfig); len(config) > 0 {
   286  		log.Printf("[INFO] Setting Lambda %s VPC config %#v from API", d.Id(), config)
   287  		err := d.Set("vpc_config", config)
   288  		if err != nil {
   289  			return fmt.Errorf("Failed setting vpc_config: %s", err)
   290  		}
   291  	}
   292  	d.Set("source_code_hash", function.CodeSha256)
   293  
   294  	// List is sorted from oldest to latest
   295  	// so this may get costly over time :'(
   296  	var lastVersion, lastQualifiedArn string
   297  	err = listVersionsByFunctionPages(conn, &lambda.ListVersionsByFunctionInput{
   298  		FunctionName: function.FunctionName,
   299  		MaxItems:     aws.Int64(10000),
   300  	}, func(p *lambda.ListVersionsByFunctionOutput, lastPage bool) bool {
   301  		if lastPage {
   302  			last := p.Versions[len(p.Versions)-1]
   303  			lastVersion = *last.Version
   304  			lastQualifiedArn = *last.FunctionArn
   305  			return true
   306  		}
   307  		return false
   308  	})
   309  	if err != nil {
   310  		return err
   311  	}
   312  
   313  	d.Set("version", lastVersion)
   314  	d.Set("qualified_arn", lastQualifiedArn)
   315  
   316  	return nil
   317  }
   318  
   319  func listVersionsByFunctionPages(c *lambda.Lambda, input *lambda.ListVersionsByFunctionInput,
   320  	fn func(p *lambda.ListVersionsByFunctionOutput, lastPage bool) bool) error {
   321  	for {
   322  		page, err := c.ListVersionsByFunction(input)
   323  		if err != nil {
   324  			return err
   325  		}
   326  		lastPage := page.NextMarker == nil
   327  
   328  		shouldContinue := fn(page, lastPage)
   329  		if !shouldContinue || lastPage {
   330  			break
   331  		}
   332  	}
   333  	return nil
   334  }
   335  
   336  // resourceAwsLambdaFunction maps to:
   337  // DeleteFunction in the API / SDK
   338  func resourceAwsLambdaFunctionDelete(d *schema.ResourceData, meta interface{}) error {
   339  	conn := meta.(*AWSClient).lambdaconn
   340  
   341  	log.Printf("[INFO] Deleting Lambda Function: %s", d.Id())
   342  
   343  	params := &lambda.DeleteFunctionInput{
   344  		FunctionName: aws.String(d.Get("function_name").(string)),
   345  	}
   346  
   347  	_, err := conn.DeleteFunction(params)
   348  	if err != nil {
   349  		return fmt.Errorf("Error deleting Lambda Function: %s", err)
   350  	}
   351  
   352  	d.SetId("")
   353  
   354  	return nil
   355  }
   356  
   357  // resourceAwsLambdaFunctionUpdate maps to:
   358  // UpdateFunctionCode in the API / SDK
   359  func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) error {
   360  	conn := meta.(*AWSClient).lambdaconn
   361  
   362  	d.Partial(true)
   363  
   364  	if d.HasChange("filename") || d.HasChange("source_code_hash") || d.HasChange("s3_bucket") || d.HasChange("s3_key") || d.HasChange("s3_object_version") {
   365  		codeReq := &lambda.UpdateFunctionCodeInput{
   366  			FunctionName: aws.String(d.Id()),
   367  			Publish:      aws.Bool(d.Get("publish").(bool)),
   368  		}
   369  
   370  		if v, ok := d.GetOk("filename"); ok {
   371  			// Grab an exclusive lock so that we're only reading one function into
   372  			// memory at a time.
   373  			// See https://github.com/hashicorp/terraform/issues/9364
   374  			awsMutexKV.Lock(awsMutexLambdaKey)
   375  			defer awsMutexKV.Unlock(awsMutexLambdaKey)
   376  			file, err := loadFileContent(v.(string))
   377  			if err != nil {
   378  				return fmt.Errorf("Unable to load %q: %s", v.(string), err)
   379  			}
   380  			codeReq.ZipFile = file
   381  		} else {
   382  			s3Bucket, _ := d.GetOk("s3_bucket")
   383  			s3Key, _ := d.GetOk("s3_key")
   384  			s3ObjectVersion, versionOk := d.GetOk("s3_object_version")
   385  
   386  			codeReq.S3Bucket = aws.String(s3Bucket.(string))
   387  			codeReq.S3Key = aws.String(s3Key.(string))
   388  			if versionOk {
   389  				codeReq.S3ObjectVersion = aws.String(s3ObjectVersion.(string))
   390  			}
   391  		}
   392  
   393  		log.Printf("[DEBUG] Send Update Lambda Function Code request: %#v", codeReq)
   394  
   395  		_, err := conn.UpdateFunctionCode(codeReq)
   396  		if err != nil {
   397  			return fmt.Errorf("Error modifying Lambda Function Code %s: %s", d.Id(), err)
   398  		}
   399  
   400  		d.SetPartial("filename")
   401  		d.SetPartial("source_code_hash")
   402  		d.SetPartial("s3_bucket")
   403  		d.SetPartial("s3_key")
   404  		d.SetPartial("s3_object_version")
   405  	}
   406  
   407  	configReq := &lambda.UpdateFunctionConfigurationInput{
   408  		FunctionName: aws.String(d.Id()),
   409  	}
   410  
   411  	configUpdate := false
   412  	if d.HasChange("description") {
   413  		configReq.Description = aws.String(d.Get("description").(string))
   414  		configUpdate = true
   415  	}
   416  	if d.HasChange("handler") {
   417  		configReq.Handler = aws.String(d.Get("handler").(string))
   418  		configUpdate = true
   419  	}
   420  	if d.HasChange("memory_size") {
   421  		configReq.MemorySize = aws.Int64(int64(d.Get("memory_size").(int)))
   422  		configUpdate = true
   423  	}
   424  	if d.HasChange("role") {
   425  		configReq.Role = aws.String(d.Get("role").(string))
   426  		configUpdate = true
   427  	}
   428  	if d.HasChange("timeout") {
   429  		configReq.Timeout = aws.Int64(int64(d.Get("timeout").(int)))
   430  		configUpdate = true
   431  	}
   432  
   433  	if configUpdate {
   434  		log.Printf("[DEBUG] Send Update Lambda Function Configuration request: %#v", configReq)
   435  		_, err := conn.UpdateFunctionConfiguration(configReq)
   436  		if err != nil {
   437  			return fmt.Errorf("Error modifying Lambda Function Configuration %s: %s", d.Id(), err)
   438  		}
   439  		d.SetPartial("description")
   440  		d.SetPartial("handler")
   441  		d.SetPartial("memory_size")
   442  		d.SetPartial("role")
   443  		d.SetPartial("timeout")
   444  	}
   445  	d.Partial(false)
   446  
   447  	return resourceAwsLambdaFunctionRead(d, meta)
   448  }
   449  
   450  // loadFileContent returns contents of a file in a given path
   451  func loadFileContent(v string) ([]byte, error) {
   452  	filename, err := homedir.Expand(v)
   453  	if err != nil {
   454  		return nil, err
   455  	}
   456  	fileContent, err := ioutil.ReadFile(filename)
   457  	if err != nil {
   458  		return nil, err
   459  	}
   460  	return fileContent, nil
   461  }
   462  
   463  func validateVPCConfig(v interface{}) (map[string]interface{}, error) {
   464  	configs := v.([]interface{})
   465  	if len(configs) > 1 {
   466  		return nil, errors.New("Only a single vpc_config block is expected")
   467  	}
   468  
   469  	config, ok := configs[0].(map[string]interface{})
   470  
   471  	if !ok {
   472  		return nil, errors.New("vpc_config is <nil>")
   473  	}
   474  
   475  	// if subnet_ids and security_group_ids are both empty then the VPC is optional
   476  	if config["subnet_ids"].(*schema.Set).Len() == 0 && config["security_group_ids"].(*schema.Set).Len() == 0 {
   477  		return nil, nil
   478  	}
   479  
   480  	if config["subnet_ids"].(*schema.Set).Len() == 0 {
   481  		return nil, errors.New("vpc_config.subnet_ids cannot be empty")
   482  	}
   483  
   484  	if config["security_group_ids"].(*schema.Set).Len() == 0 {
   485  		return nil, errors.New("vpc_config.security_group_ids cannot be empty")
   486  	}
   487  
   488  	return config, nil
   489  }