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

F aws lambda invocation crud support #29367

Merged
merged 17 commits into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .changelog/29367.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_lambda_invocation: Add lifecycle_scope CRUD to invoke on each resource state transition
```
18 changes: 18 additions & 0 deletions internal/service/lambda/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,21 @@ const (
const (
propagationTimeout = 5 * time.Minute
)

const (
invocationActionCreate = "create"
invocationActionDelete = "delete"
invocationActionUpdate = "update"
)

const (
lifecycleScopeCreateOnly = "CREATE_ONLY"
lifecycleScopeCrud = "CRUD"
)

func lifecycleScope_Values() []string {
return []string{
lifecycleScopeCreateOnly,
lifecycleScopeCrud,
}
}
33 changes: 33 additions & 0 deletions internal/service/lambda/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package lambda

import (
"context"
"errors"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// customizeDiffValidateInput validates that `input` is JSON object when
// `lifecycle_scope` is not "CREATE_ONLY"
func customizeDiffValidateInput(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
if diff.Get("lifecycle_scope") == lifecycleScopeCreateOnly {
return nil
}
// input is validated to be valid JSON in the schema already.
inputNoSpaces := strings.TrimSpace(diff.Get("input").(string))
if strings.HasPrefix(inputNoSpaces, "{") && strings.HasSuffix(inputNoSpaces, "}") {
return nil
}

return errors.New(`lifecycle_scope other than "CREATE_ONLY" requires input to be a JSON object`)
}

// customizeDiffInputChangeWithCreateOnlyScope forces a new resource when `input` has
// a change and `lifecycle_scope` is set to "CREATE_ONLY"
func customizeDiffInputChangeWithCreateOnlyScope(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
if diff.HasChange("input") && diff.Get("lifecycle_scope").(string) == lifecycleScopeCreateOnly {
return diff.ForceNew("input")
}
return nil
}
123 changes: 110 additions & 13 deletions internal/service/lambda/invocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,29 @@ package lambda
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"log"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/lambda"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag"
)

const defaultInvocationTerraformKey = "tf"

// @SDKResource("aws_lambda_invocation")
func ResourceInvocation() *schema.Resource {
return &schema.Resource{
CreateWithoutTimeout: resourceInvocationCreate,
ReadWithoutTimeout: resourceInvocationRead,
DeleteWithoutTimeout: resourceInvocationDelete,
UpdateWithoutTimeout: resourceInvocationUpdate,

Schema: map[string]*schema.Schema{
"function_name": {
Expand All @@ -31,7 +36,6 @@ func ResourceInvocation() *schema.Resource {
"input": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringIsJSON,
},
"qualifier": {
Expand All @@ -50,17 +54,121 @@ func ResourceInvocation() *schema.Resource {
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"lifecycle_scope": {
Type: schema.TypeString,
Optional: true,
Default: lifecycleScopeCreateOnly,
ValidateFunc: validation.StringInSlice(lifecycleScope_Values(), false),
},
"terraform_key": {
Type: schema.TypeString,
Optional: true,
Default: defaultInvocationTerraformKey,
},
},
CustomizeDiff: customdiff.Sequence(
customizeDiffValidateInput,
customizeDiffInputChangeWithCreateOnlyScope,
),
}
}

func resourceInvocationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return invoke(ctx, invocationActionCreate, d, meta)
}

func resourceInvocationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics
return diags
}

func resourceInvocationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return invoke(ctx, invocationActionUpdate, d, meta)
}

func resourceInvocationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
if !isCreateOnlyScope(d) {
log.Printf("[DEBUG] Lambda Invocation (%s) \"deleted\" by invocation & removing from state", d.Id())
return invoke(ctx, invocationActionDelete, d, meta)
}
var diags diag.Diagnostics
log.Printf("[DEBUG] Lambda Invocation (%s) \"deleted\" by removing from state", d.Id())
return diags
}

// buildInput makes sure that the user provided input is enriched for handling lifecycle events
//
// In order to make this a non-breaking change this function only manipulates input if
// the invocation is not only for creation of resources. In order for the lambda
// to understand the action it has to take we pass on the action that terraform wants to do
// on the invocation resource.
//
// Because Lambda functions by default are stateless we must pass the input from the previous
// invocation to allow implementation of delete/update at Lambda side.
func buildInput(d *schema.ResourceData, action string) ([]byte, error) {
if isCreateOnlyScope(d) {
jsonBytes := []byte(d.Get("input").(string))
return jsonBytes, nil
}
oldInputMap, newInputMap, err := getInputChange(d)
if err != nil {
log.Printf("[DEBUG] input serialization %s", err)
return nil, err
}

newInputMap[d.Get("terraform_key").(string)] = map[string]interface{}{
"action": action,
"prev_input": oldInputMap,
}
return json.Marshal(&newInputMap)
}

func getObjectFromJSONString(s string) (map[string]interface{}, error) {
if len(s) == 0 {
return nil, nil
}
var mapObject map[string]interface{}
if err := json.Unmarshal([]byte(s), &mapObject); err != nil {
log.Printf("[ERROR] input JSON deserialization '%s'", s)
return nil, err
}
return mapObject, nil
}

// getInputChange gets old an new input as maps
func getInputChange(d *schema.ResourceData) (map[string]interface{}, map[string]interface{}, error) {
old, new := d.GetChange("input")
oldMap, err := getObjectFromJSONString(old.(string))
if err != nil {
log.Printf("[ERROR] old input serialization '%s'", old.(string))
return nil, nil, err
}
newMap, err := getObjectFromJSONString(new.(string))
if err != nil {
log.Printf("[ERROR] new input serialization '%s'", new.(string))
return nil, nil, err
}
return oldMap, newMap, nil
}

// isCreateOnlyScope returns True if Lambda is only invoked when the resource is
// created or replaced.
//
// The original invocation logic only triggers on create.
func isCreateOnlyScope(d *schema.ResourceData) bool {
return d.Get("lifecycle_scope").(string) == lifecycleScopeCreateOnly
}

func invoke(ctx context.Context, action string, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).LambdaConn()

functionName := d.Get("function_name").(string)
qualifier := d.Get("qualifier").(string)
input := []byte(d.Get("input").(string))
input, err := buildInput(d, action)
if err != nil {
return sdkdiag.AppendErrorf(diags, "Lambda Invocation (%s) input transformation failed for input (%s): %s", d.Id(), d.Get("input").(string), err)
}

res, err := conn.InvokeWithContext(ctx, &lambda.InvokeInput{
FunctionName: aws.String(functionName),
Expand All @@ -82,14 +190,3 @@ func resourceInvocationCreate(ctx context.Context, d *schema.ResourceData, meta

return diags
}

func resourceInvocationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics
return diags
}

func resourceInvocationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics
log.Printf("[DEBUG] Lambda Invocation (%s) \"deleted\" by removing from state", d.Id())
return diags
}
Loading