diff --git a/.changelog/43972.txt b/.changelog/43972.txt new file mode 100644 index 000000000000..c63e8a43cc9d --- /dev/null +++ b/.changelog/43972.txt @@ -0,0 +1,3 @@ +```release-note:new-action +aws_lambda_invoke +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 026dbb81080e..ae13c0637878 100644 --- a/go.mod +++ b/go.mod @@ -289,7 +289,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.7.0 - github.com/hashicorp/hcl/v2 v2.23.0 + github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/terraform-json v0.27.2 github.com/hashicorp/terraform-plugin-framework v1.16.0 github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0 @@ -300,7 +300,7 @@ require ( github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-mux v0.21.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 - github.com/hashicorp/terraform-plugin-testing v1.13.3 + github.com/hashicorp/terraform-plugin-testing v1.13.3-0.20250909115916-1a2eeae85247 github.com/jaswdr/faker/v2 v2.8.0 github.com/jmespath/go-jmespath v0.4.0 github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38 @@ -349,7 +349,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/hc-install v0.9.2 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-exec v0.23.0 // indirect + github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 // indirect github.com/hashicorp/terraform-registry-address v0.4.0 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.2 // indirect @@ -368,7 +368,7 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/zclconf/go-cty v1.16.4 // indirect + github.com/zclconf/go-cty v1.17.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.63.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect diff --git a/go.sum b/go.sum index 45428faddc78..fa03a69bd1c7 100644 --- a/go.sum +++ b/go.sum @@ -661,12 +661,12 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= -github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= -github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= -github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY= +github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2 h1:90fcAqw0Qmv4vY7zL4jEKgKarHmOnNN6SjTY68eLKGA= +github.com/hashicorp/terraform-exec v0.23.1-0.20250717072919-061a850a52d2/go.mod h1:8D3RLLpzAZdhT9jvALYz1KHyGU4OvI73I1o0+01QJxA= github.com/hashicorp/terraform-json v0.27.2 h1:BwGuzM6iUPqf9JYM/Z4AF1OJ5VVJEEzoKST/tRDBJKU= github.com/hashicorp/terraform-json v0.27.2/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= github.com/hashicorp/terraform-plugin-framework v1.16.0 h1:tP0f+yJg0Z672e7levixDe5EpWwrTrNryPM9kDMYIpE= @@ -685,8 +685,8 @@ github.com/hashicorp/terraform-plugin-mux v0.21.0 h1:QsEYnzSD2c3zT8zUrUGqaFGhV/Z github.com/hashicorp/terraform-plugin-mux v0.21.0/go.mod h1:Qpt8+6AD7NmL0DS7ASkN0EXpDQ2J/FnnIgeUr1tzr5A= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsore1ZaRWU9cnB6jFoBnIM= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0/go.mod h1:QYmYnLfsosrxjCnGY1p9c7Zj6n9thnEE+7RObeYs3fA= -github.com/hashicorp/terraform-plugin-testing v1.13.3 h1:QLi/khB8Z0a5L54AfPrHukFpnwsGL8cwwswj4RZduCo= -github.com/hashicorp/terraform-plugin-testing v1.13.3/go.mod h1:WHQ9FDdiLoneey2/QHpGM/6SAYf4A7AZazVg7230pLE= +github.com/hashicorp/terraform-plugin-testing v1.13.3-0.20250909115916-1a2eeae85247 h1:lA6ofPwmCXAX7J7kVP9t/WMU5+eA4e9YvJUiRLPdENw= +github.com/hashicorp/terraform-plugin-testing v1.13.3-0.20250909115916-1a2eeae85247/go.mod h1:4r/7cxl1mpskfALcq58Iyu5aPiTSco8SVrKkcLyP5g4= github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk= github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= @@ -790,8 +790,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1: github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.16.4 h1:QGXaag7/7dCzb+odlGrgr+YmYZFaOCMW6DEpS+UD1eE= -github.com/zclconf/go-cty v1.16.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= +github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/internal/service/lambda/invoke_action.go b/internal/service/lambda/invoke_action.go new file mode 100644 index 000000000000..0e0fc0ed2ff2 --- /dev/null +++ b/internal/service/lambda/invoke_action.go @@ -0,0 +1,216 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lambda + +import ( + "context" + "encoding/base64" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/lambda" + awstypes "github.com/aws/aws-sdk-go-v2/service/lambda/types" + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/framework/validators" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @Action(aws_lambda_invoke, name="Invoke") +func newInvokeAction(_ context.Context) (action.ActionWithConfigure, error) { + return &invokeAction{}, nil +} + +var ( + _ action.Action = (*invokeAction)(nil) +) + +type invokeAction struct { + framework.ActionWithModel[invokeActionModel] +} + +type invokeActionModel struct { + framework.WithRegionModel + FunctionName types.String `tfsdk:"function_name"` + Payload types.String `tfsdk:"payload"` + Qualifier types.String `tfsdk:"qualifier"` + InvocationType fwtypes.StringEnum[awstypes.InvocationType] `tfsdk:"invocation_type"` + LogType fwtypes.StringEnum[awstypes.LogType] `tfsdk:"log_type"` + ClientContext types.String `tfsdk:"client_context"` +} + +func (a *invokeAction) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Invokes an AWS Lambda function with the specified payload. This action allows for imperative invocation of Lambda functions with full control over invocation parameters.", + Attributes: map[string]schema.Attribute{ + "function_name": schema.StringAttribute{ + Description: "The name, ARN, or partial ARN of the Lambda function to invoke. You can specify a function name (e.g., my-function), a qualified function name (e.g., my-function:PROD), or a partial ARN (e.g., 123456789012:function:my-function).", + Required: true, + }, + "payload": schema.StringAttribute{ + Description: "The JSON payload to send to the Lambda function. This should be a valid JSON string that represents the event data for your function.", + Required: true, + Validators: []validator.String{ + validators.JSON(), + }, + }, + "qualifier": schema.StringAttribute{ + Description: "The version or alias of the Lambda function to invoke. If not specified, the $LATEST version will be invoked.", + Optional: true, + }, + "invocation_type": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.InvocationType](), + Description: "The invocation type. Valid values are 'RequestResponse' (synchronous), 'Event' (asynchronous), and 'DryRun' (validate parameters without invoking). Defaults to 'RequestResponse'.", + Optional: true, + }, + "log_type": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.LogType](), + Description: "Set to 'Tail' to include the execution log in the response. Only applies to synchronous invocations ('RequestResponse' invocation type). Defaults to 'None'.", + Optional: true, + }, + "client_context": schema.StringAttribute{ + Description: "Up to 3,583 bytes of base64-encoded data about the invoking client to pass to the function in the context object. This is only used for mobile applications.", + Optional: true, + }, + }, + } +} + +func (a *invokeAction) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + var config invokeActionModel + + // Parse configuration + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + // Get AWS client + conn := a.Meta().LambdaClient(ctx) + + functionName := config.FunctionName.ValueString() + payload := config.Payload.ValueString() + + // Set default values for optional parameters + invocationType := awstypes.InvocationTypeRequestResponse + if !config.InvocationType.IsNull() && !config.InvocationType.IsUnknown() { + invocationType = config.InvocationType.ValueEnum() + } + + logType := awstypes.LogTypeNone + if !config.LogType.IsNull() && !config.LogType.IsUnknown() { + logType = config.LogType.ValueEnum() + } + + tflog.Info(ctx, "Starting Lambda function invocation action", map[string]any{ + "function_name": functionName, + "invocation_type": string(invocationType), + "log_type": string(logType), + "payload_length": len(payload), + "has_qualifier": !config.Qualifier.IsNull(), + "has_client_context": !config.ClientContext.IsNull(), + }) + + // Send initial progress update + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("Invoking Lambda function %s...", functionName), + }) + + // Build the invoke input + input := &lambda.InvokeInput{ + FunctionName: aws.String(functionName), + Payload: []byte(payload), + InvocationType: invocationType, + LogType: logType, + } + + // Set optional parameters + if !config.Qualifier.IsNull() { + input.Qualifier = config.Qualifier.ValueStringPointer() + } + + if !config.ClientContext.IsNull() { + clientContext := config.ClientContext.ValueString() + // Validate that client context is base64 encoded + if _, err := base64.StdEncoding.DecodeString(clientContext); err != nil { + resp.Diagnostics.AddError( + "Invalid Client Context", + fmt.Sprintf("Client context must be base64 encoded: %s", err), + ) + return + } + input.ClientContext = aws.String(clientContext) + } + + // Perform the invocation + output, err := conn.Invoke(ctx, input) + if err != nil { + resp.Diagnostics.AddError( + "Failed to Invoke Lambda Function", + fmt.Sprintf("Could not invoke Lambda function %s: %s", functionName, err), + ) + return + } + + // Handle function errors + if output.FunctionError != nil { + functionError := aws.ToString(output.FunctionError) + payloadStr := string(output.Payload) + + resp.Diagnostics.AddError( + "Lambda Function Execution Error", + fmt.Sprintf("Lambda function %s returned an error (%s): %s", functionName, functionError, payloadStr), + ) + return + } + + // Handle different invocation types + switch invocationType { + case awstypes.InvocationTypeRequestResponse: + // For synchronous invocations, we get an immediate response + statusCode := output.StatusCode + payloadLength := len(output.Payload) + + var message string + if logType == awstypes.LogTypeTail && output.LogResult != nil { + message = fmt.Sprintf("Lambda function %s invoked successfully (status: %d, payload: %d bytes, logs included)", + functionName, statusCode, payloadLength) + } else { + message = fmt.Sprintf("Lambda function %s invoked successfully (status: %d, payload: %d bytes)", + functionName, statusCode, payloadLength) + } + + resp.SendProgress(action.InvokeProgressEvent{ + Message: message, + }) + + case awstypes.InvocationTypeEvent: + // For asynchronous invocations, we only get confirmation that the request was accepted + statusCode := output.StatusCode + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("Lambda function %s invoked asynchronously (status: %d)", functionName, statusCode), + }) + + case awstypes.InvocationTypeDryRun: + // For dry run, we validate parameters without actually invoking + statusCode := output.StatusCode + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("Lambda function %s dry run completed successfully (status: %d)", functionName, statusCode), + }) + } + + tflog.Info(ctx, "Lambda function invocation action completed successfully", map[string]any{ + "function_name": functionName, + "invocation_type": string(invocationType), + names.AttrStatusCode: output.StatusCode, + "executed_version": aws.ToString(output.ExecutedVersion), + "has_logs": output.LogResult != nil, + "payload_length": len(output.Payload), + }) +} diff --git a/internal/service/lambda/invoke_action_test.go b/internal/service/lambda/invoke_action_test.go new file mode 100644 index 000000000000..ffedccd8a10e --- /dev/null +++ b/internal/service/lambda/invoke_action_test.go @@ -0,0 +1,561 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lambda_test + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/lambda" + awstypes "github.com/aws/aws-sdk-go-v2/service/lambda/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccLambdaInvokeAction_basic(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + testData := "value3" + inputJSON := `{"key1":"value1","key2":"value2"}` + expectedResult := fmt.Sprintf(`{"key1":"value1","key2":"value2","key3":%q}`, testData) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.LambdaEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.LambdaServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccInvokeActionConfig_basic(rName, testData, inputJSON), + Check: resource.ComposeTestCheckFunc( + testAccCheckInvokeAction(ctx, rName, inputJSON, expectedResult), + ), + }, + }, + }) +} + +func TestAccLambdaInvokeAction_withQualifier(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + testData := "qualifier_test" + inputJSON := `{"key1":"value1","key2":"value2"}` + expectedResult := fmt.Sprintf(`{"key1":"value1","key2":"value2","key3":%q}`, testData) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.LambdaEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.LambdaServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccInvokeActionConfig_withQualifier(rName, testData, inputJSON), + Check: resource.ComposeTestCheckFunc( + testAccCheckInvokeActionWithQualifier(ctx, rName, inputJSON, expectedResult), + ), + }, + }, + }) +} + +func TestAccLambdaInvokeAction_invocationTypes(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + testData := "invocation_types_test" + inputJSON := `{"key1":"value1","key2":"value2"}` + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.LambdaEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.LambdaServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccInvokeActionConfig_invocationType(rName, testData, inputJSON, "RequestResponse"), + Check: resource.ComposeTestCheckFunc( + testAccCheckInvokeActionInvocationType(ctx, rName, inputJSON, awstypes.InvocationTypeRequestResponse), + ), + }, + { + Config: testAccInvokeActionConfig_invocationType(rName, testData, inputJSON, "Event"), + Check: resource.ComposeTestCheckFunc( + testAccCheckInvokeActionInvocationType(ctx, rName, inputJSON, awstypes.InvocationTypeEvent), + ), + }, + { + Config: testAccInvokeActionConfig_invocationType(rName, testData, inputJSON, "DryRun"), + Check: resource.ComposeTestCheckFunc( + testAccCheckInvokeActionInvocationType(ctx, rName, inputJSON, awstypes.InvocationTypeDryRun), + ), + }, + }, + }) +} + +func TestAccLambdaInvokeAction_logTypes(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + testData := "log_types_test" + inputJSON := `{"key1":"value1","key2":"value2"}` + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.LambdaEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.LambdaServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccInvokeActionConfig_logType(rName, testData, inputJSON, "None"), + Check: resource.ComposeTestCheckFunc( + testAccCheckInvokeActionLogType(ctx, rName, inputJSON, awstypes.LogTypeNone), + ), + }, + { + Config: testAccInvokeActionConfig_logType(rName, testData, inputJSON, "Tail"), + Check: resource.ComposeTestCheckFunc( + testAccCheckInvokeActionLogType(ctx, rName, inputJSON, awstypes.LogTypeTail), + ), + }, + }, + }) +} + +func TestAccLambdaInvokeAction_clientContext(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + testData := "client_context_test" + inputJSON := `{"key1":"value1","key2":"value2"}` + clientContext := base64.StdEncoding.EncodeToString([]byte(`{"client":{"client_id":"test_client","app_version":"1.0.0"},"env":{"locale":"en_US"}}`)) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.LambdaEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.LambdaServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccInvokeActionConfig_clientContext(rName, testData, inputJSON, clientContext), + Check: resource.ComposeTestCheckFunc( + testAccCheckInvokeActionClientContext(ctx, rName, inputJSON, clientContext), + ), + }, + }, + }) +} + +func TestAccLambdaInvokeAction_complexPayload(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + testData := "complex_test" + inputJSON := `{"key1":{"subkey1":"subvalue1"},"key2":{"subkey2":"subvalue2","subkey3":{"a":"b"}}}` + expectedResult := fmt.Sprintf(`{"key1":{"subkey1":"subvalue1"},"key2":{"subkey2":"subvalue2","subkey3":{"a":"b"}},"key3":%q}`, testData) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.LambdaEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.LambdaServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccInvokeActionConfig_basic(rName, testData, inputJSON), + Check: resource.ComposeTestCheckFunc( + testAccCheckInvokeAction(ctx, rName, inputJSON, expectedResult), + ), + }, + }, + }) +} + +// Test helper functions + +// testAccCheckInvokeAction verifies that the action can successfully invoke a Lambda function +func testAccCheckInvokeAction(ctx context.Context, functionName, inputJSON, expectedResult string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).LambdaClient(ctx) + + // Invoke the function directly to verify it's working and compare results + input := &lambda.InvokeInput{ + FunctionName: &functionName, + InvocationType: awstypes.InvocationTypeRequestResponse, + Payload: []byte(inputJSON), + } + + output, err := conn.Invoke(ctx, input) + if err != nil { + return fmt.Errorf("Failed to invoke Lambda function %s: %w", functionName, err) + } + + if output.FunctionError != nil { + return fmt.Errorf("Lambda function %s returned an error: %s", functionName, string(output.Payload)) + } + + actualResult := string(output.Payload) + if actualResult != expectedResult { + return fmt.Errorf("Lambda function %s result mismatch. Expected: %s, Got: %s", functionName, expectedResult, actualResult) + } + + return nil + } +} + +// testAccCheckInvokeActionWithQualifier verifies action works with function qualifiers +func testAccCheckInvokeActionWithQualifier(ctx context.Context, functionName, inputJSON, expectedResult string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).LambdaClient(ctx) + + // Get the function to retrieve the version + getFunc, err := conn.GetFunction(ctx, &lambda.GetFunctionInput{ + FunctionName: &functionName, + }) + if err != nil { + return fmt.Errorf("Failed to get Lambda function %s: %w", functionName, err) + } + + // Invoke with the specific version + input := &lambda.InvokeInput{ + FunctionName: &functionName, + InvocationType: awstypes.InvocationTypeRequestResponse, + Payload: []byte(inputJSON), + Qualifier: getFunc.Configuration.Version, + } + + output, err := conn.Invoke(ctx, input) + if err != nil { + return fmt.Errorf("Failed to invoke Lambda function %s with qualifier: %w", functionName, err) + } + + if output.FunctionError != nil { + return fmt.Errorf("Lambda function %s returned an error: %s", functionName, string(output.Payload)) + } + + actualResult := string(output.Payload) + if actualResult != expectedResult { + return fmt.Errorf("Lambda function %s result mismatch with qualifier. Expected: %s, Got: %s", functionName, expectedResult, actualResult) + } + + return nil + } +} + +// testAccCheckInvokeActionInvocationType verifies different invocation types work +func testAccCheckInvokeActionInvocationType(ctx context.Context, functionName, inputJSON string, invocationType awstypes.InvocationType) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).LambdaClient(ctx) + + input := &lambda.InvokeInput{ + FunctionName: &functionName, + InvocationType: invocationType, + Payload: []byte(inputJSON), + } + + output, err := conn.Invoke(ctx, input) + if err != nil { + return fmt.Errorf("Failed to invoke Lambda function %s with invocation type %s: %w", functionName, string(invocationType), err) + } + + // For async invocations, we just verify the request was accepted + if invocationType == awstypes.InvocationTypeEvent { + if output.StatusCode != http.StatusAccepted { + return fmt.Errorf("Expected status code 202 for async invocation, got %d", output.StatusCode) + } + } + + // For dry run, we verify the function would execute successfully + if invocationType == awstypes.InvocationTypeDryRun { + if output.StatusCode != http.StatusNoContent { + return fmt.Errorf("Expected status code 204 for dry run, got %d", output.StatusCode) + } + } + + return nil + } +} + +// testAccCheckInvokeActionLogType verifies log type configuration works +func testAccCheckInvokeActionLogType(ctx context.Context, functionName, inputJSON string, logType awstypes.LogType) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).LambdaClient(ctx) + + input := &lambda.InvokeInput{ + FunctionName: &functionName, + InvocationType: awstypes.InvocationTypeRequestResponse, + Payload: []byte(inputJSON), + LogType: logType, + } + + output, err := conn.Invoke(ctx, input) + if err != nil { + return fmt.Errorf("Failed to invoke Lambda function %s with log type %s: %w", functionName, string(logType), err) + } + + if output.FunctionError != nil { + return fmt.Errorf("Lambda function %s returned an error: %s", functionName, string(output.Payload)) + } + + // If log type is Tail, we should have log results + if logType == awstypes.LogTypeTail { + if output.LogResult == nil { + return fmt.Errorf("Expected log result when log type is Tail, but got none") + } + } + + return nil + } +} + +// testAccCheckInvokeActionClientContext verifies client context is passed correctly +func testAccCheckInvokeActionClientContext(ctx context.Context, functionName, inputJSON, clientContext string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).LambdaClient(ctx) + + input := &lambda.InvokeInput{ + FunctionName: &functionName, + InvocationType: awstypes.InvocationTypeRequestResponse, + Payload: []byte(inputJSON), + ClientContext: &clientContext, + } + + output, err := conn.Invoke(ctx, input) + if err != nil { + return fmt.Errorf("Failed to invoke Lambda function %s with client context: %w", functionName, err) + } + + if output.FunctionError != nil { + return fmt.Errorf("Lambda function %s returned an error: %s", functionName, string(output.Payload)) + } + + return nil + } +} + +// Configuration functions + +func testAccInvokeActionConfig_base(rName string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +data "aws_iam_policy_document" "test" { + statement { + effect = "Allow" + actions = ["sts:AssumeRole"] + principals { + type = "Service" + identifiers = ["lambda.${data.aws_partition.current.dns_suffix}"] + } + } +} + +resource "aws_iam_role" "test" { + name = %[1]q + assume_role_policy = data.aws_iam_policy_document.test.json +} + +resource "aws_iam_role_policy_attachment" "test" { + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + role = aws_iam_role.test.name +} +`, rName) +} + +func testAccInvokeActionConfig_function(rName, testData string) string { + return acctest.ConfigCompose( + testAccInvokeActionConfig_base(rName), + fmt.Sprintf(` +resource "aws_lambda_function" "test" { + depends_on = [aws_iam_role_policy_attachment.test] + + filename = "test-fixtures/lambda_invocation.zip" + function_name = %[1]q + role = aws_iam_role.test.arn + handler = "lambda_invocation.handler" + runtime = "nodejs18.x" + + environment { + variables = { + TEST_DATA = %[2]q + } + } +} +`, rName, testData)) +} + +func testAccInvokeActionConfig_basic(rName, testData, inputJSON string) string { + return acctest.ConfigCompose( + testAccInvokeActionConfig_function(rName, testData), + fmt.Sprintf(` +action "aws_lambda_invoke" "test" { + config { + function_name = aws_lambda_function.test.function_name + payload = %[1]q + } +} + +resource "terraform_data" "trigger" { + input = "trigger" + lifecycle { + action_trigger { + events = [before_create, before_update] + actions = [action.aws_lambda_invoke.test] + } + } +} +`, inputJSON)) +} + +func testAccInvokeActionConfig_withQualifier(rName, testData, inputJSON string) string { + return acctest.ConfigCompose( + testAccInvokeActionConfig_base(rName), + fmt.Sprintf(` +resource "aws_lambda_function" "test" { + depends_on = [aws_iam_role_policy_attachment.test] + + filename = "test-fixtures/lambda_invocation.zip" + function_name = %[1]q + role = aws_iam_role.test.arn + handler = "lambda_invocation.handler" + runtime = "nodejs18.x" + publish = true + + environment { + variables = { + TEST_DATA = %[2]q + } + } +} + +action "aws_lambda_invoke" "test" { + config { + function_name = aws_lambda_function.test.function_name + payload = %[3]q + qualifier = aws_lambda_function.test.version + } +} + +resource "terraform_data" "trigger" { + input = "trigger" + lifecycle { + action_trigger { + events = [before_create, before_update] + actions = [action.aws_lambda_invoke.test] + } + } +} +`, rName, testData, inputJSON)) +} + +func testAccInvokeActionConfig_invocationType(rName, testData, inputJSON, invocationType string) string { + return acctest.ConfigCompose( + testAccInvokeActionConfig_function(rName, testData), + fmt.Sprintf(` +action "aws_lambda_invoke" "test" { + config { + function_name = aws_lambda_function.test.function_name + payload = %[1]q + invocation_type = %[2]q + } +} + +resource "terraform_data" "trigger" { + input = "trigger" + lifecycle { + action_trigger { + events = [before_create, before_update] + actions = [action.aws_lambda_invoke.test] + } + } +} +`, inputJSON, invocationType)) +} + +func testAccInvokeActionConfig_logType(rName, testData, inputJSON, logType string) string { + return acctest.ConfigCompose( + testAccInvokeActionConfig_function(rName, testData), + fmt.Sprintf(` +action "aws_lambda_invoke" "test" { + config { + function_name = aws_lambda_function.test.function_name + payload = %[1]q + log_type = %[2]q + } +} + +resource "terraform_data" "trigger" { + input = "trigger" + lifecycle { + action_trigger { + events = [before_create, before_update] + actions = [action.aws_lambda_invoke.test] + } + } +} +`, inputJSON, logType)) +} + +func testAccInvokeActionConfig_clientContext(rName, testData, inputJSON, clientContext string) string { + return acctest.ConfigCompose( + testAccInvokeActionConfig_function(rName, testData), + fmt.Sprintf(` +action "aws_lambda_invoke" "test" { + config { + function_name = aws_lambda_function.test.function_name + payload = %[1]q + client_context = %[2]q + } +} + +resource "terraform_data" "trigger" { + input = "trigger" + lifecycle { + action_trigger { + events = [before_create, before_update] + actions = [action.aws_lambda_invoke.test] + } + } +} +`, inputJSON, clientContext)) +} diff --git a/internal/service/lambda/service_package_gen.go b/internal/service/lambda/service_package_gen.go index cc19d311790d..291f5f67a8fd 100644 --- a/internal/service/lambda/service_package_gen.go +++ b/internal/service/lambda/service_package_gen.go @@ -17,6 +17,16 @@ import ( type servicePackage struct{} +func (p *servicePackage) Actions(ctx context.Context) []*inttypes.ServicePackageAction { + return []*inttypes.ServicePackageAction{ + { + Factory: newInvokeAction, + TypeName: "aws_lambda_invoke", + Name: "Invoke", + Region: unique.Make(inttypes.ResourceRegionDefault()), + }, + } +} func (p *servicePackage) EphemeralResources(ctx context.Context) []*inttypes.ServicePackageEphemeralResource { return []*inttypes.ServicePackageEphemeralResource{ { diff --git a/website/docs/actions/lambda_invoke.html.markdown b/website/docs/actions/lambda_invoke.html.markdown new file mode 100644 index 000000000000..4a048c106e8f --- /dev/null +++ b/website/docs/actions/lambda_invoke.html.markdown @@ -0,0 +1,221 @@ +--- +subcategory: "Lambda" +layout: "aws" +page_title: "AWS: aws_lambda_invoke" +description: |- + Invokes an AWS Lambda function with the specified payload. +--- + +# Action: aws_lambda_invoke + +~> **Note:** `aws_lambda_invoke` is in beta. Its interface and behavior may change as the feature evolves, and breaking changes are possible. It is offered as a technical preview without compatibility guarantees until Terraform 1.14 is generally available. + +Invokes an AWS Lambda function with the specified payload. This action allows for imperative invocation of Lambda functions with full control over invocation parameters. + +For information about AWS Lambda functions, see the [AWS Lambda Developer Guide](https://docs.aws.amazon.com/lambda/latest/dg/). For specific information about invoking Lambda functions, see the [Invoke](https://docs.aws.amazon.com/lambda/latest/api/API_Invoke.html) page in the AWS Lambda API Reference. + +~> **Note:** Synchronous invocations will wait for the function to complete execution, while asynchronous invocations return immediately after the request is _accepted_. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_lambda_function" "example" { + # ... function configuration +} + +action "aws_lambda_invoke" "example" { + config { + function_name = aws_lambda_function.example.function_name + payload = jsonencode({ + key1 = "value1" + key2 = "value2" + }) + } +} + +resource "terraform_data" "example" { + input = "trigger-lambda" + + lifecycle { + action_trigger { + events = [before_create, before_update] + actions = [action.aws_lambda_invoke.example] + } + } +} +``` + +### Invoke with Function Version + +```terraform +action "aws_lambda_invoke" "versioned" { + config { + function_name = aws_lambda_function.example.function_name + qualifier = aws_lambda_function.example.version + payload = jsonencode({ + operation = "process" + data = var.processing_data + }) + } +} +``` + +### Asynchronous Invocation + +```terraform +action "aws_lambda_invoke" "async" { + config { + function_name = aws_lambda_function.worker.function_name + invocation_type = "Event" + payload = jsonencode({ + task_id = "background-job-${random_uuid.job_id.result}" + data = local.background_task_data + }) + } +} +``` + +### Dry Run Validation + +```terraform +action "aws_lambda_invoke" "validate" { + config { + function_name = aws_lambda_function.validator.function_name + invocation_type = "DryRun" + payload = jsonencode({ + config = var.validation_config + }) + } +} +``` + +### With Log Capture + +```terraform +action "aws_lambda_invoke" "debug" { + config { + function_name = aws_lambda_function.debug.function_name + log_type = "Tail" + payload = jsonencode({ + debug_level = "verbose" + component = "api-gateway" + }) + } +} +``` + +### Mobile Application Context + +```terraform +action "aws_lambda_invoke" "mobile" { + config { + function_name = aws_lambda_function.mobile_backend.function_name + client_context = base64encode(jsonencode({ + client = { + client_id = "mobile-app" + app_version = "1.2.3" + } + env = { + locale = "en_US" + } + })) + payload = jsonencode({ + user_id = var.user_id + action = "sync_data" + }) + } +} +``` + +### CI/CD Pipeline Integration + +Use this action in your deployment pipeline to trigger post-deployment functions: + +```terraform +# Trigger warmup after deployment +resource "terraform_data" "deploy_complete" { + input = local.deployment_id + + lifecycle { + action_trigger { + events = [before_create, before_update] + actions = [action.aws_lambda_invoke.warmup] + } + } + + depends_on = [aws_lambda_function.api] +} + +action "aws_lambda_invoke" "warmup" { + config { + function_name = aws_lambda_function.api.function_name + payload = jsonencode({ + action = "warmup" + source = "terraform-deployment" + }) + } +} +``` + +### Environment-Specific Processing + +```terraform +locals { + processing_config = var.environment == "production" ? { + batch_size = 100 + timeout = 900 + } : { + batch_size = 10 + timeout = 60 + } +} + +action "aws_lambda_invoke" "process_data" { + config { + function_name = aws_lambda_function.processor.function_name + payload = jsonencode(merge(local.processing_config, { + data_source = var.data_source + environment = var.environment + })) + } +} +``` + +### Complex Payload with Dynamic Content + +```terraform +action "aws_lambda_invoke" "complex" { + config { + function_name = aws_lambda_function.orchestrator.function_name + payload = jsonencode({ + workflow = { + id = "workflow-${timestamp()}" + steps = var.workflow_steps + } + resources = { + s3_bucket = aws_s3_bucket.data.bucket + dynamodb = aws_dynamodb_table.state.name + sns_topic = aws_sns_topic.notifications.arn + } + metadata = { + created_by = "terraform" + environment = var.environment + version = var.app_version + } + }) + } +} +``` + +## Argument Reference + +This action supports the following arguments: + +* `client_context` - (Optional) Up to 3,583 bytes of base64-encoded data about the invoking client to pass to the function in the context object. This is only used for mobile applications and should contain information about the client application and device. +* `function_name` - (Required) Name, ARN, or partial ARN of the Lambda function to invoke. You can specify a function name (e.g., `my-function`), a qualified function name (e.g., `my-function:PROD`), or a partial ARN (e.g., `123456789012:function:my-function`). +* `invocation_type` - (Optional) Invocation type. Valid values are `RequestResponse` (default) for synchronous invocation that waits for the function to complete and returns the response, `Event` for asynchronous invocation that returns immediately after the request is accepted, and `DryRun` to validate parameters and verify permissions without actually executing the function. +* `log_type` - (Optional) Set to `Tail` to include the execution log in the response. Only applies to synchronous invocations (`RequestResponse` invocation type). Defaults to `None`. When set to `Tail`, the last 4 KB of the execution log is included in the response. +* `payload` - (Required) JSON payload to send to the Lambda function. This should be a valid JSON string that represents the event data for your function. The payload size limit is 6 MB for synchronous invocations and 256 KB for asynchronous invocations. +* `qualifier` - (Optional) Version or alias of the Lambda function to invoke. If not specified, the `$LATEST` version will be invoked. Can be a version number (e.g., `1`) or an alias (e.g., `PROD`).