diff --git a/.changelog/39988.txt b/.changelog/39988.txt new file mode 100644 index 000000000000..4f241851be63 --- /dev/null +++ b/.changelog/39988.txt @@ -0,0 +1,3 @@ +```release-note:new-ephemeral +aws_lambda_invocation +``` diff --git a/internal/create/errors.go b/internal/create/errors.go index becfaa3f4bd4..7fbaf09cfd1a 100644 --- a/internal/create/errors.go +++ b/internal/create/errors.go @@ -19,17 +19,22 @@ const ( ErrActionCheckingExistence = "checking existence" ErrActionCheckingNotRecreated = "checking not recreated" ErrActionCheckingRecreated = "checking recreated" + ErrActionClosing = "closing" + ErrActionConfiguring = "configuring" ErrActionCreating = "creating" ErrActionDeleting = "deleting" + ErrActionExpandingResourceId = "expanding resource id" + ErrActionFlatteningResourceId = "flattening resource id" ErrActionImporting = "importing" + ErrActionOpening = "opening" ErrActionReading = "reading" + ErrActionRenewing = "renewing" ErrActionSetting = "setting" ErrActionUpdating = "updating" + ErrActionValidating = "validating" ErrActionWaitingForCreation = "waiting for creation" ErrActionWaitingForDeletion = "waiting for delete" ErrActionWaitingForUpdate = "waiting for update" - ErrActionExpandingResourceId = "expanding resource id" - ErrActionFlatteningResourceId = "flattening resource id" ) // ProblemStandardMessage is a standardized message for reporting errors and warnings diff --git a/internal/service/lambda/invocation_ephemeral.go b/internal/service/lambda/invocation_ephemeral.go new file mode 100644 index 000000000000..492abc07454a --- /dev/null +++ b/internal/service/lambda/invocation_ephemeral.go @@ -0,0 +1,139 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lambda + +import ( + "context" + "errors" + + "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/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + 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" +) + +// @EphemeralResource("aws_lambda_invocation", name="Invocation") +func newEphemeralInvocation(_ context.Context) (ephemeral.EphemeralResourceWithConfigure, error) { + return &ephemeralInvocation{}, nil +} + +const ( + ResNameInvocation = "Invocation" +) + +type ephemeralInvocation struct { + framework.EphemeralResourceWithConfigure +} + +func (e *ephemeralInvocation) Metadata(_ context.Context, _ ephemeral.MetadataRequest, response *ephemeral.MetadataResponse) { + response.TypeName = "aws_lambda_invocation" +} + +func (e *ephemeralInvocation) Schema(ctx context.Context, _ ephemeral.SchemaRequest, response *ephemeral.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "client_context": schema.StringAttribute{ + Optional: true, + }, + "executed_version": schema.StringAttribute{ + Computed: true, + }, + "function_error": schema.StringAttribute{ + Computed: true, + }, + "function_name": schema.StringAttribute{ + Required: true, + }, + "log_result": schema.StringAttribute{ + Computed: true, + }, + "log_type": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.LogType](), + Optional: true, + }, + "payload": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + validators.JSON(), + }, + }, + "qualifier": schema.StringAttribute{ + Optional: true, + }, + "result": schema.StringAttribute{ + Computed: true, + }, + names.AttrStatusCode: schema.Int32Attribute{ + Computed: true, + }, + }, + } +} + +func (e *ephemeralInvocation) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + conn := e.Meta().LambdaClient(ctx) + data := epInvocationData{} + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + input := &lambda.InvokeInput{ + InvocationType: awstypes.InvocationTypeRequestResponse, + } + resp.Diagnostics.Append(flex.Expand(ctx, data, input)...) + if resp.Diagnostics.HasError() { + return + } + + if input.FunctionName == nil { + data.Result = types.StringValue("") + resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...) + return + } + + output, err := conn.Invoke(ctx, input) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Lambda, create.ErrActionOpening, ResNameInvocation, data.FunctionName.String(), err), + err.Error(), + ) + return + } + + if output.FunctionError != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.Lambda, create.ErrActionOpening, ResNameInvocation, data.FunctionName.String(), errors.New(aws.ToString(output.FunctionError))), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, output, &data)...) + data.Result = flex.StringValueToFramework(ctx, string(output.Payload)) + resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...) +} + +type epInvocationData struct { + ClientContext types.String `tfsdk:"client_context"` + ExecutedVersion types.String `tfsdk:"executed_version"` + FunctionError types.String `tfsdk:"function_error"` + FunctionName types.String `tfsdk:"function_name"` + LogResult types.String `tfsdk:"log_result"` + LogType fwtypes.StringEnum[awstypes.LogType] `tfsdk:"log_type"` + Payload types.String `tfsdk:"payload"` + Qualifier types.String `tfsdk:"qualifier"` + Result types.String `tfsdk:"result"` + StatusCode types.Int32 `tfsdk:"status_code"` +} diff --git a/internal/service/lambda/invocation_ephemeral_test.go b/internal/service/lambda/invocation_ephemeral_test.go new file mode 100644 index 000000000000..04dc3ae00251 --- /dev/null +++ b/internal/service/lambda/invocation_ephemeral_test.go @@ -0,0 +1,97 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lambda_test + +import ( + "fmt" + "math/big" + "testing" + + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccLambdaInvocationEphemeral_basic(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + echoResourceName := "echo.test" + dp := tfjsonpath.New("data") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.LambdaServiceID), + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories(ctx, acctest.ProviderNameEcho), + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccInvocationEphemeralConfig_basic(rName), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(echoResourceName, dp.AtMapKey("executed_version"), knownvalue.StringExact("$LATEST")), + statecheck.ExpectKnownValue(echoResourceName, dp.AtMapKey("function_name"), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dp.AtMapKey("log_result"), knownvalue.Null()), + statecheck.ExpectKnownValue(echoResourceName, dp.AtMapKey("result"), knownvalue.StringExact(`{"key1":"value1","key2":"value2"}`)), + statecheck.ExpectKnownValue(echoResourceName, dp.AtMapKey(names.AttrStatusCode), knownvalue.NumberExact(big.NewFloat(200))), + }, + }, + }, + }) +} + +func testAccInvocationEphemeralConfig_basic(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigWithEchoProvider("ephemeral.aws_lambda_invocation.test"), + 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 +} + +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" +} + +ephemeral "aws_lambda_invocation" "test" { + function_name = aws_lambda_function.test.arn + + payload = jsonencode({ + key1 = "value1" + key2 = "value2" + }) +} +`, rName)) +} diff --git a/internal/service/lambda/service_package_gen.go b/internal/service/lambda/service_package_gen.go index b787967d5a9e..08cd796328fb 100644 --- a/internal/service/lambda/service_package_gen.go +++ b/internal/service/lambda/service_package_gen.go @@ -14,6 +14,15 @@ import ( type servicePackage struct{} +func (p *servicePackage) EphemeralResources(ctx context.Context) []*types.ServicePackageEphemeralResource { + return []*types.ServicePackageEphemeralResource{ + { + Factory: newEphemeralInvocation, + Name: "Invocation", + }, + } +} + func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.ServicePackageFrameworkDataSource { return []*types.ServicePackageFrameworkDataSource{} } diff --git a/website/docs/ephemeral-resources/kms_secrets.html.markdown b/website/docs/ephemeral-resources/kms_secrets.html.markdown index 7d712b513694..0361903540d6 100644 --- a/website/docs/ephemeral-resources/kms_secrets.html.markdown +++ b/website/docs/ephemeral-resources/kms_secrets.html.markdown @@ -10,6 +10,8 @@ description: |- Decrypt multiple secrets from data encrypted with the AWS KMS service. +~> **NOTE:** Ephemeral resources are a new feature and may evolve as we continue to explore their most effective uses. [Learn more](https://developer.hashicorp.com/terraform/language/v1.10.x/resources/ephemeral). + ## Example Usage If you do not already have a `CiphertextBlob` from encrypting a KMS secret, you can use the below commands to obtain one using the [AWS CLI kms encrypt](https://docs.aws.amazon.com/cli/latest/reference/kms/encrypt.html) command. This requires you to have your AWS CLI setup correctly and replace the `--key-id` with your own. Alternatively you can use `--plaintext 'master-password'` (CLIv1) or `--plaintext fileb://<(echo -n 'master-password')` (CLIv2) instead of reading from a file. diff --git a/website/docs/ephemeral-resources/lambda_invocation.html.markdown b/website/docs/ephemeral-resources/lambda_invocation.html.markdown new file mode 100644 index 000000000000..2f1c8097ab53 --- /dev/null +++ b/website/docs/ephemeral-resources/lambda_invocation.html.markdown @@ -0,0 +1,60 @@ +--- +subcategory: "Lambda" +layout: "aws" +page_title: "AWS: aws_lambda_invocation" +description: |- + Invoke AWS Lambda Function +--- + +# Ephemeral: aws_lambda_invocation + +Use this ephemeral resource to invoke a Lambda function. The lambda function is invoked with the [RequestResponse](https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html#API_Invoke_RequestSyntax) invocation type. + +~> **NOTE:** Ephemeral resources are a new feature and may evolve as we continue to explore their most effective uses. [Learn more](https://developer.hashicorp.com/terraform/language/v1.10.x/resources/ephemeral). + +~> **NOTE:** The `aws_lambda_invocation` ephemeral resource invokes the function during every `plan` and `apply` when the function is known. A common use case for this functionality is when invoking a lightweight function—where repeated invocations are acceptable—that produces sensitive information you do not want to store in the state. + +~> **NOTE:** If you get a `KMSAccessDeniedException: Lambda was unable to decrypt the environment variables because KMS access was denied` error when invoking an [`aws_lambda_function`](/docs/providers/aws/r/lambda_function.html) with environment variables, the IAM role associated with the function may have been deleted and recreated _after_ the function was created. You can fix the problem two ways: 1) updating the function's role to another role and then updating it back again to the recreated role, or 2) by using Terraform to `taint` the function and `apply` your configuration again to recreate the function. (When you create a function, Lambda grants permissions on the KMS key to the function's IAM role. If the IAM role is recreated, the grant is no longer valid. Changing the function's role or recreating the function causes Lambda to update the grant.) + +## Example Usage + +### Basic Example + +```terraform +ephemeral "aws_lambda_invocation" "example" { + function_name = aws_lambda_function.lambda_function_test.function_name + + payload = jsonencode({ + key1 = "value1" + key2 = "value2" + }) +} + +output "result_entry" { + value = jsondecode(aws_lambda_invocation.example.result)["key1"] + ephemeral = true +} +``` + +## Argument Reference + +The following arguments are required: + +* `function_name` - (Required) Name or ARN of the Lambda function, version, or alias. You can append a version number or alias. If you specify only the function name, it is limited to 64 characters in length. +* `payload` - (Required) JSON that you want to provide to your Lambda function as input. + +The following arguments are optional: + +* `client_context` - (Optional) Up to 3583 bytes of base64-encoded data about the invoking client to pass to the function in the context object. +* `log_type` - (Optional) Set to `Tail` to include the execution log in the response. Valid values are `None` and `Tail`. +* `qualifier` - (Optional) Version or alias to invoke a published version of the function. Defaults to `$LATEST`. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `executed_version` - Version of the function that executed. When you invoke a function with an alias, the version the alias resolved to. +* `function_error` - If present, indicates that an error occurred during function execution. Details about the error are included in `result`. +* `log_result` - Last 4 KB of the execution log, which is base64-encoded. +* `result` - String result of the lambda function invocation. +* `status_code` - HTTP status code is in the 200 range for a successful request. diff --git a/website/docs/ephemeral-resources/secretsmanager_secret_version.html.markdown b/website/docs/ephemeral-resources/secretsmanager_secret_version.html.markdown index 66dce4bd7730..e847bc3ec70a 100644 --- a/website/docs/ephemeral-resources/secretsmanager_secret_version.html.markdown +++ b/website/docs/ephemeral-resources/secretsmanager_secret_version.html.markdown @@ -10,6 +10,8 @@ description: |- Retrieve information about a Secrets Manager secret version, including its secret value. To retrieve secret metadata, see the [`aws_secretsmanager_secret` data source](/docs/providers/aws/d/secretsmanager_secret.html). +~> **NOTE:** Ephemeral resources are a new feature and may evolve as we continue to explore their most effective uses. [Learn more](https://developer.hashicorp.com/terraform/language/v1.10.x/resources/ephemeral). + ## Example Usage ### Retrieve Current Secret Version