Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lambda container image support #16512

Merged
merged 18 commits into from
Dec 1, 2020
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 153 additions & 19 deletions aws/resource_aws_lambda_function.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"io/ioutil"
"log"
"regexp"
Expand Down Expand Up @@ -47,22 +48,57 @@ func resourceAwsLambdaFunction() *schema.Resource {
"filename": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"s3_bucket", "s3_key", "s3_object_version"},
ConflictsWith: []string{"s3_bucket", "s3_key", "s3_object_version", "image_uri"},
Copy link
Collaborator

@DrFaust92 DrFaust92 Dec 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a good chance to substitute with ExactlyOneOf: []string{"filename", "s3_bucket", "image_uri"} this will save the custom check below.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. We should probably also set RequiredWith: []string{"s3_key"} on s3_bucket

},
"s3_bucket": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"filename"},
ConflictsWith: []string{"filename", "image_uri"},
},
"s3_key": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"filename"},
ConflictsWith: []string{"filename", "image_uri"},
},
"s3_object_version": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"filename"},
ConflictsWith: []string{"filename", "image_uri"},
},
"image_uri": {
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"filename", "s3_bucket", "s3_key", "s3_object_version"},
},
"package_type": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this just be computed based on s3 / code or image?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, you can now end up in a situation where a user will choose a ZIP and an image which isn't correct

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I know there's a validator but why even need it)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if AWS adds support for another archive type later on? im assuming the API was designed to be open for expansion in the future, we should follow the API. https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/ and such...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. We typically don't automatically set parameters like this. We should, however, add validation along the lines of:

  • if package_type is Image, image_uri must be set and none of filename or s3_* can be set
  • if package_type is Zip, image_uri and image_config cannot be set and either filename or some combination of s3_* must be set

Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: lambda.PackageTypeZip,
ValidateFunc: validation.StringInSlice(lambda.PackageType_Values(), false),
},
"image_config": {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesnt conflict with zip type arguments?

Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"entry_point": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"command": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"working_directory": {
Type: schema.TypeString,
Optional: true,
},
},
},
},
"code_signing_config_arn": {
Type: schema.TypeString,
Expand Down Expand Up @@ -126,7 +162,7 @@ func resourceAwsLambdaFunction() *schema.Resource {
},
"handler": {
Type: schema.TypeString,
Required: true,
Optional: true,
ValidateFunc: validation.StringLenBetween(1, 128),
},
"layers": {
Expand Down Expand Up @@ -155,7 +191,7 @@ func resourceAwsLambdaFunction() *schema.Resource {
},
"runtime": {
Type: schema.TypeString,
Required: true,
Optional: true,
ValidateFunc: validation.StringInSlice(lambda.Runtime_Values(), false),
},
"timeout": {
Expand Down Expand Up @@ -280,10 +316,24 @@ func resourceAwsLambdaFunction() *schema.Resource {
"tags": tagsSchema(),
},

CustomizeDiff: updateComputedAttributesOnPublish,
CustomizeDiff: customdiff.Sequence(
checkHandlerRuntimeForZipFunction,
updateComputedAttributesOnPublish,
),
}
}

func checkHandlerRuntimeForZipFunction(_ context.Context, d *schema.ResourceDiff, meta interface{}) error {
packageType := d.Get("package_type")
_, handlerOk := d.GetOk("handler")
_, runtimeOk := d.GetOk("runtime")

if packageType == lambda.PackageTypeZip && !handlerOk && !runtimeOk {
return fmt.Errorf("handler and runtime must be set when PackageType is Zip")
}
return nil
}

func updateComputedAttributesOnPublish(_ context.Context, d *schema.ResourceDiff, meta interface{}) error {
configChanged := hasConfigChanges(d)
functionCodeUpdated := needsFunctionCodeUpdate(d)
Expand All @@ -304,6 +354,7 @@ func hasConfigChanges(d resourceDiffer) bool {
return d.HasChange("description") ||
d.HasChange("handler") ||
d.HasChange("file_system_config") ||
d.HasChange("image_config") ||
d.HasChange("memory_size") ||
d.HasChange("role") ||
d.HasChange("timeout") ||
Expand Down Expand Up @@ -331,9 +382,10 @@ func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) e
s3Bucket, bucketOk := d.GetOk("s3_bucket")
s3Key, keyOk := d.GetOk("s3_key")
s3ObjectVersion, versionOk := d.GetOk("s3_object_version")
imageUri, hasImageUri := d.GetOk("image_uri")

if !hasFilename && !bucketOk && !keyOk && !versionOk {
return errors.New("filename or s3_* attributes must be set")
if !hasFilename && !bucketOk && !keyOk && !versionOk && !hasImageUri {
return errors.New("filename, s3_* or image_uri attributes must be set")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is causing a failure in the test TestAccAWSLambdaFunction_expectFilenameAndS3Attributes

}

var functionCode *lambda.FunctionCode
Expand All @@ -350,6 +402,10 @@ func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) e
functionCode = &lambda.FunctionCode{
ZipFile: file,
}
} else if hasImageUri {
functionCode = &lambda.FunctionCode{
ImageUri: aws.String(imageUri.(string)),
}
} else {
if !bucketOk || !keyOk {
return errors.New("s3_bucket and s3_key must all be set while using S3 code source")
Expand All @@ -363,16 +419,28 @@ func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) e
}
}

packageType := d.Get("package_type")
handler, handlerOk := d.GetOk("handler")
runtime, runtimeOk := d.GetOk("runtime")

if packageType == lambda.PackageTypeZip && !handlerOk && !runtimeOk {
return errors.New("handler and runtime must be set when PackageType is Zip")
}

params := &lambda.CreateFunctionInput{
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)),
PackageType: aws.String(d.Get("package_type").(string)),
}

if packageType == lambda.PackageTypeZip {
params.Handler = aws.String(handler.(string))
params.Runtime = aws.String(runtime.(string))
}

if v, ok := d.GetOk("code_signing_config_arn"); ok {
Expand Down Expand Up @@ -401,6 +469,10 @@ func resourceAwsLambdaFunctionCreate(d *schema.ResourceData, meta interface{}) e
params.FileSystemConfigs = expandLambdaFileSystemConfigs(v.([]interface{}))
}

if v, ok := d.GetOk("image_config"); ok && len(v.([]interface{})) > 0 {
params.ImageConfig = expandLambdaImageConfigs(v.([]interface{}))
}

if v, ok := d.GetOk("vpc_config"); ok && len(v.([]interface{})) > 0 {
config := v.([]interface{})[0].(map[string]interface{})

Expand Down Expand Up @@ -641,6 +713,23 @@ func resourceAwsLambdaFunctionRead(d *schema.ResourceData, meta interface{}) err
return fmt.Errorf("Error setting file system config for Lambda Function (%s): %w", d.Id(), err)
}

// Add Package Type
log.Printf("[INFO] Setting Lambda %s package type %#v from API", d.Id(), function.PackageType)
if err := d.Set("package_type", function.PackageType); err != nil {
return fmt.Errorf("Error setting package type for Lambda Function: %w", err)
}

// Add Image Configuration
imageConfig := flattenLambdaImageConfig(function.ImageConfigResponse)
log.Printf("[INFO] Setting Lambda %s Image config %#v from API", d.Id(), imageConfig)
if err := d.Set("image_config", imageConfig); err != nil {
return fmt.Errorf("Error setting image config for Lambda Function: %s", err)
}

if err := d.Set("image_uri", getFunctionOutput.Code.ImageUri); err != nil {
return fmt.Errorf("Error setting image uri for Lambda Function: %s", err)
}

layers := flattenLambdaLayers(function.Layers)
log.Printf("[INFO] Setting Lambda %s Layers %#v from API", d.Id(), layers)
if err := d.Set("layers", layers); err != nil {
Expand Down Expand Up @@ -723,15 +812,20 @@ func resourceAwsLambdaFunctionRead(d *schema.ResourceData, meta interface{}) err
FunctionName: aws.String(d.Get("function_name").(string)),
}

getCodeSigningConfigOutput, err := conn.GetFunctionCodeSigningConfig(codeSigningConfigInput)
if err != nil {
return fmt.Errorf("error getting Lambda Function (%s) code signing config %w", d.Id(), err)
}
// Code Signing is only supported on zip packaged lambda functions.
if *function.PackageType == lambda.PackageTypeZip {
getCodeSigningConfigOutput, err := conn.GetFunctionCodeSigningConfig(codeSigningConfigInput)
if err != nil {
return fmt.Errorf("error getting Lambda Function (%s) code signing config %w", d.Id(), err)
}

if getCodeSigningConfigOutput == nil || getCodeSigningConfigOutput.CodeSigningConfigArn == nil {
d.Set("code_signing_config_arn", "")
if getCodeSigningConfigOutput == nil || getCodeSigningConfigOutput.CodeSigningConfigArn == nil {
d.Set("code_signing_config_arn", "")
} else {
d.Set("code_signing_config_arn", getCodeSigningConfigOutput.CodeSigningConfigArn)
}
} else {
d.Set("code_signing_config_arn", getCodeSigningConfigOutput.CodeSigningConfigArn)
d.Set("code_signing_config_arn", "")
}

return nil
Expand Down Expand Up @@ -779,7 +873,9 @@ func needsFunctionCodeUpdate(d resourceDiffer) bool {
d.HasChange("source_code_hash") ||
d.HasChange("s3_bucket") ||
d.HasChange("s3_key") ||
d.HasChange("s3_object_version")
d.HasChange("s3_object_version") ||
d.HasChange("image_uri")

}

// resourceAwsLambdaFunctionUpdate maps to:
Expand Down Expand Up @@ -840,6 +936,12 @@ func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) e
configReq.FileSystemConfigs = expandLambdaFileSystemConfigs(v.([]interface{}))
}
}
if d.HasChange("image_config") {
configReq.ImageConfig = &lambda.ImageConfig{}
if v, ok := d.GetOk("image_config"); ok && len(v.([]interface{})) > 0 {
configReq.ImageConfig = expandLambdaImageConfigs(v.([]interface{}))
}
}
if d.HasChange("memory_size") {
configReq.MemorySize = aws.Int64(int64(d.Get("memory_size").(int)))
}
Expand Down Expand Up @@ -990,6 +1092,8 @@ func resourceAwsLambdaFunctionUpdate(d *schema.ResourceData, meta interface{}) e
return fmt.Errorf("Unable to load %q: %w", v.(string), err)
}
codeReq.ZipFile = file
} else if v, ok := d.GetOk("image_uri"); ok {
codeReq.ImageUri = aws.String(v.(string))
} else {
s3Bucket, _ := d.GetOk("s3_bucket")
s3Key, _ := d.GetOk("s3_key")
Expand Down Expand Up @@ -1188,3 +1292,33 @@ func expandLambdaFileSystemConfigs(fscMaps []interface{}) []*lambda.FileSystemCo
}
return fileSystemConfigs
}

func flattenLambdaImageConfig(response *lambda.ImageConfigResponse) []map[string]interface{} {
settings := make(map[string]interface{})

if response == nil || response.Error != nil {
return nil
}

settings["command"] = response.ImageConfig.Command
settings["entry_point"] = response.ImageConfig.EntryPoint
settings["working_directory"] = response.ImageConfig.WorkingDirectory

return []map[string]interface{}{settings}
}

func expandLambdaImageConfigs(imageConfigMaps []interface{}) *lambda.ImageConfig {
imageConfig := &lambda.ImageConfig{}
// only one image_config block is allowed
if len(imageConfigMaps) == 1 && imageConfigMaps[0] != nil {
config := imageConfigMaps[0].(map[string]interface{})
if len(config["entry_point"].([]interface{})) > 0 {
imageConfig.EntryPoint = expandStringList(config["entry_point"].([]interface{}))
}
if len(config["command"].([]interface{})) > 0 {
imageConfig.Command = expandStringList(config["command"].([]interface{}))
}
imageConfig.WorkingDirectory = aws.String(config["working_directory"].(string))
}
return imageConfig
}
Loading