Skip to content

Commit

Permalink
Merge pull request #817 from kbalk/dryrun
Browse files Browse the repository at this point in the history
Add function to invoke a lambda using an invocation type of "DryRun"
  • Loading branch information
brikis98 authored Apr 6, 2021
2 parents 2e4cdf8 + f10e0c2 commit a87d10f
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 2 deletions.
114 changes: 114 additions & 0 deletions modules/aws/lambda.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,66 @@ package aws

import (
"encoding/json"
"errors"
"fmt"

"github.com/aws/aws-sdk-go/service/lambda"
"github.com/gruntwork-io/terratest/modules/testing"
"github.com/stretchr/testify/require"
)

type InvocationTypeOption string

const (
InvocationTypeRequestResponse InvocationTypeOption = "RequestResponse"
InvocationTypeDryRun = "DryRun"
)

func (itype *InvocationTypeOption) Value() (string, error) {
if itype != nil {
switch *itype {
case
InvocationTypeRequestResponse,
InvocationTypeDryRun:
return string(*itype), nil
default:
msg := fmt.Sprintf("LambdaOptions.InvocationType, if specified, must either be \"%s\" or \"%s\"",
InvocationTypeRequestResponse,
InvocationTypeDryRun)
return "", errors.New(msg)
}
}
return string(InvocationTypeRequestResponse), nil
}

// LambdaOptions contains additional parameters for InvokeFunctionWithParams().
// It contains a subset of the fields found in the lambda.InvokeInput struct.
type LambdaOptions struct {
// InvocationType can be one of InvocationTypeOption values:
// * InvocationTypeRequestResponse (default) - Invoke the function
// synchronously. Keep the connection open until the function
// returns a response or times out.
// * InvocationTypeDryRun - Validate parameter values and verify
// that the user or role has permission to invoke the function.
InvocationType *InvocationTypeOption

// Lambda function input; will be converted to JSON.
Payload interface{}
}

// LambdaOutput contains the output from InvokeFunctionWithParams(). The
// fields may or may not have a value depending on the invocation type and
// whether an error occurred or not.
type LambdaOutput struct {
// The response from the function, or an error object.
Payload []byte

// The HTTP status code for a successful request is in the 200 range.
// For RequestResponse invocation type, the status code is 200.
// For the DryRun invocation type, the status code is 204.
StatusCode *int64
}

// InvokeFunction invokes a lambda function.
func InvokeFunction(t testing.TestingT, region, functionName string, payload interface{}) []byte {
out, err := InvokeFunctionE(t, region, functionName, payload)
Expand Down Expand Up @@ -49,6 +102,67 @@ func InvokeFunctionE(t testing.TestingT, region, functionName string, payload in
return out.Payload, nil
}

// InvokeFunctionWithParams invokes a lambda function using parameters
// supplied in the LambdaOptions struct and returns values in a LambdaOutput
// struct. Checks for failure using "require".
func InvokeFunctionWithParams(t testing.TestingT, region, functionName string, input *LambdaOptions) *LambdaOutput {
out, err := InvokeFunctionWithParamsE(t, region, functionName, input)
require.NoError(t, err)
return out
}

// InvokeFunctionWithParamsE invokes a lambda function using parameters
// supplied in the LambdaOptions struct. Returns the status code and payload
// in a LambdaOutput struct and the error. A non-nil error will either reflect
// a problem with the parameters supplied to this function or an error returned
// by the Lambda.
func InvokeFunctionWithParamsE(t testing.TestingT, region, functionName string, input *LambdaOptions) (*LambdaOutput, error) {
lambdaClient, err := NewLambdaClientE(t, region)
if err != nil {
return nil, err
}

// Verify the InvocationType is one of the allowed values and report
// an error if it's not. By default the InvocationType will be
// "RequestResponse".
invocationType, err := input.InvocationType.Value()
if err != nil {
return nil, err
}

invokeInput := &lambda.InvokeInput{
FunctionName: &functionName,
InvocationType: &invocationType,
}

if input.Payload != nil {
payloadJson, err := json.Marshal(input.Payload)
if err != nil {
return nil, err
}
invokeInput.Payload = payloadJson
}

out, err := lambdaClient.Invoke(invokeInput)
if err != nil {
return nil, err
}

// As this function supports different invocation types, it must
// then support different combinations of output other than just
// payload.
lambdaOutput := LambdaOutput{
Payload: out.Payload,
StatusCode: out.StatusCode,
}

if out.FunctionError != nil {
return &lambdaOutput, errors.New(*out.FunctionError)
}

return &lambdaOutput, nil
}

type FunctionError struct {
Message string
StatusCode int64
Expand Down
88 changes: 86 additions & 2 deletions test/terraform_aws_lambda_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -15,6 +16,10 @@ import (
func TestTerraformAwsLambdaExample(t *testing.T) {
t.Parallel()

// Make a copy of the terraform module to a temporary directory. This allows running multiple tests in parallel
// against the same terraform module.
exampleFolder := test_structure.CopyTerraformFolderToTemp(t, "../", "examples/terraform-aws-lambda-example")

// Give this lambda function a unique ID for a name so we can distinguish it from any other lambdas
// in your AWS account
functionName := fmt.Sprintf("terratest-aws-lambda-example-%s", random.UniqueId())
Expand All @@ -26,7 +31,7 @@ func TestTerraformAwsLambdaExample(t *testing.T) {
// terraform testing.
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
// The path to where our Terraform code is located
TerraformDir: "../examples/terraform-aws-lambda-example",
TerraformDir: exampleFolder,

// Variables to pass to our Terraform code using -var options
Vars: map[string]interface{}{
Expand All @@ -52,7 +57,7 @@ func TestTerraformAwsLambdaExample(t *testing.T) {
assert.Equal(t, `"hi!"`, string(response))

// Invoke the function, this time causing it to error and capturing the error
response, err := aws.InvokeFunctionE(t, awsRegion, functionName, ExampleFunctionPayload{ShouldFail: true, Echo: "hi!"})
_, err := aws.InvokeFunctionE(t, awsRegion, functionName, ExampleFunctionPayload{ShouldFail: true, Echo: "hi!"})

// Function-specific errors have their own special return
functionError, ok := err.(*aws.FunctionError)
Expand All @@ -62,6 +67,85 @@ func TestTerraformAwsLambdaExample(t *testing.T) {
assert.Contains(t, string(functionError.Payload), "Failed to handle")
}

// Annother example of how to test the Terraform module in
// examples/terraform-aws-lambda-example using Terratest, this time with
// the aws.InvokeFunctionWithParams.
func TestTerraformAwsLambdaWithParamsExample(t *testing.T) {
t.Parallel()

// Make a copy of the terraform module to a temporary directory. This allows running multiple tests in parallel
// against the same terraform module.
exampleFolder := test_structure.CopyTerraformFolderToTemp(t, "../", "examples/terraform-aws-lambda-example")

// Give this lambda function a unique ID for a name so we can distinguish it from any other lambdas
// in your AWS account
functionName := fmt.Sprintf("terratest-aws-lambda-withparams-example-%s", random.UniqueId())

// Pick a random AWS region to test in. This helps ensure your code works in all regions.
awsRegion := aws.GetRandomStableRegion(t, nil, nil)

// Construct the terraform options with default retryable errors to handle the most common retryable errors in
// terraform testing.
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
// The path to where our Terraform code is located
TerraformDir: exampleFolder,

// Variables to pass to our Terraform code using -var options
Vars: map[string]interface{}{
"function_name": functionName,
},

// Environment variables to set when running Terraform
EnvVars: map[string]string{
"AWS_DEFAULT_REGION": awsRegion,
},
})

// At the end of the test, run `terraform destroy` to clean up any resources that were created
defer terraform.Destroy(t, terraformOptions)

// This will run `terraform init` and `terraform apply` and fail the test if there are any errors
terraform.InitAndApply(t, terraformOptions)

// Call InvokeFunctionWithParms with an InvocationType of "DryRun".
// A "DryRun" invocation does not execute the function, so the example
// test function will not be checking the payload.
var invocationType aws.InvocationTypeOption = aws.InvocationTypeDryRun
input := &aws.LambdaOptions{InvocationType: &invocationType}
out := aws.InvokeFunctionWithParams(t, awsRegion, functionName, input)

// With "DryRun", there's no message in the output, but there is
// a status code which will have a value of 204 for a successful
// invocation.
assert.Equal(t, int(*out.StatusCode), 204)

// Invoke the function, this time causing the Lambda to error and
// capturing the error.
invocationType = aws.InvocationTypeRequestResponse
input = &aws.LambdaOptions{
InvocationType: &invocationType,
Payload: ExampleFunctionPayload{ShouldFail: true, Echo: "hi!"},
}
out, err := aws.InvokeFunctionWithParamsE(t, awsRegion, functionName, input)

// The Lambda executed, but should have failed.
assert.Error(t, err, "Unhandled")

// Make sure the function-specific error comes back
assert.Contains(t, string(out.Payload), "Failed to handle")

// Call InvokeFunctionWithParamsE with a LambdaOptions struct that has
// an unsupported InvocationType. The function should fail.
invocationType = "Event"
input = &aws.LambdaOptions{
InvocationType: &invocationType,
Payload: ExampleFunctionPayload{ShouldFail: false, Echo: "hi!"},
}
out, err = aws.InvokeFunctionWithParamsE(t, awsRegion, functionName, input)
require.NotNil(t, err)
assert.Contains(t, err.Error(), "LambdaOptions.InvocationType, if specified, must either be \"RequestResponse\" or \"DryRun\"")
}

type ExampleFunctionPayload struct {
Echo string
ShouldFail bool
Expand Down

0 comments on commit a87d10f

Please sign in to comment.