Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changelog/45359.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/aws_lambda_function: Add `durable_config` argument
```

```release-note:enhancement
data-source/aws_lambda_function: Add `durable_config` attribute
```
197 changes: 197 additions & 0 deletions internal/service/lambda/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,26 @@ func resourceFunction() *schema.Resource {
Type: schema.TypeString,
Optional: true,
},
"durable_config": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"execution_timeout": {
Type: schema.TypeInt,
Required: true,
ValidateFunc: validation.IntBetween(1, 31622400),
},
names.AttrRetentionPeriod: {
Type: schema.TypeInt,
Optional: true,
Default: 14,
ValidateFunc: validation.IntBetween(1, 90),
},
},
},
},
names.AttrEnvironment: {
Type: schema.TypeList,
Optional: true,
Expand Down Expand Up @@ -528,6 +548,13 @@ func resourceFunction() *schema.Resource {
CustomizeDiff: customdiff.Sequence(
checkHandlerRuntimeForZipFunction,
updateComputedAttributesOnPublish,
customdiff.ForceNewIfChange("durable_config", func(_ context.Context, old, new, meta any) bool {
// Force new when durable_config is being added (from empty to non-empty) or removed (from non-empty to empty)
// Allow updates to execution_timeout and retention_period when durable_config already exists
oldLen := len(old.([]any))
newLen := len(new.([]any))
return (oldLen == 0 && newLen > 0) || (oldLen > 0 && newLen == 0)
}),
),
}
}
Expand Down Expand Up @@ -595,6 +622,10 @@ func resourceFunctionCreate(ctx context.Context, d *schema.ResourceData, meta an
}
}

if v, ok := d.GetOk("durable_config"); ok && len(v.([]any)) > 0 {
input.DurableConfig = expandDurableConfigs(v.([]any))
}

if v, ok := d.GetOk(names.AttrEnvironment); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil {
if v, ok := v.([]any)[0].(map[string]any)["variables"].(map[string]any); ok && len(v) > 0 {
input.Environment = &awstypes.Environment{
Expand Down Expand Up @@ -765,6 +796,13 @@ func resourceFunctionRead(ctx context.Context, d *schema.ResourceData, meta any)
} else {
d.Set("dead_letter_config", []any{})
}
if function.DurableConfig != nil {
if err := d.Set("durable_config", flattenDurableConfig(function.DurableConfig)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting durable_config: %s", err)
}
} else {
d.Set("durable_config", []any{})
}
d.Set(names.AttrDescription, function.Description)
if err := d.Set(names.AttrEnvironment, flattenEnvironment(function.Environment)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting environment: %s", err)
Expand Down Expand Up @@ -951,6 +989,12 @@ func resourceFunctionUpdate(ctx context.Context, d *schema.ResourceData, meta an
}
}

if d.HasChange("durable_config") {
if v, ok := d.GetOk("durable_config"); ok && len(v.([]any)) > 0 {
input.DurableConfig = expandDurableConfigs(v.([]any))
}
}

if d.HasChange(names.AttrDescription) {
input.Description = aws.String(d.Get(names.AttrDescription).(string))
}
Expand Down Expand Up @@ -1190,6 +1234,13 @@ func resourceFunctionDelete(ctx context.Context, d *schema.ResourceData, meta an
}
}

// Stop any running durable executions before deleting the function
if v, ok := d.GetOk("durable_config"); ok && len(v.([]any)) > 0 {
if err := stopDurableExecutions(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil {
return sdkdiag.AppendErrorf(diags, "stopping durable executions for Lambda Function (%s): %s", d.Id(), err)
}
}

log.Printf("[INFO] Deleting Lambda Function: %s", d.Id())
input := lambda.DeleteFunctionInput{
FunctionName: aws.String(d.Id()),
Expand All @@ -1206,9 +1257,102 @@ func resourceFunctionDelete(ctx context.Context, d *schema.ResourceData, meta an
return sdkdiag.AppendErrorf(diags, "deleting Lambda Function (%s): %s", d.Id(), err)
}

if _, err := waitFunctionDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil {
return sdkdiag.AppendErrorf(diags, "waiting for Lambda Function (%s) delete: %s", d.Id(), err)
}

return diags
}

func stopDurableExecutions(ctx context.Context, conn *lambda.Client, functionName string, timeout time.Duration) error {
input := &lambda.ListDurableExecutionsByFunctionInput{
FunctionName: aws.String(functionName),
Statuses: []awstypes.ExecutionStatus{awstypes.ExecutionStatusRunning},
}

paginator := lambda.NewListDurableExecutionsByFunctionPaginator(conn, input)
for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
if err != nil {
return err
}

for _, execution := range page.DurableExecutions {
_, err := conn.StopDurableExecution(ctx, &lambda.StopDurableExecutionInput{
DurableExecutionArn: execution.DurableExecutionArn,
})
if err != nil {
return err
}

if _, err := waitDurableExecutionStopped(ctx, conn, aws.ToString(execution.DurableExecutionArn), timeout); err != nil {
return fmt.Errorf("waiting for durable execution (%s) to stop: %w", aws.ToString(execution.DurableExecutionArn), err)
}
}
}

return nil
}

func findDurableExecution(ctx context.Context, conn *lambda.Client, arn string) (*lambda.GetDurableExecutionOutput, error) {
input := &lambda.GetDurableExecutionInput{
DurableExecutionArn: aws.String(arn),
}

output, err := conn.GetDurableExecution(ctx, input)

if errs.IsA[*awstypes.ResourceNotFoundException](err) {
return nil, &retry.NotFoundError{
LastError: err,
LastRequest: input,
}
}

if err != nil {
return nil, err
}

if output == nil {
return nil, tfresource.NewEmptyResultError(input)
}

return output, nil
}

func statusDurableExecution(ctx context.Context, conn *lambda.Client, arn string) retry.StateRefreshFunc {
return func() (any, string, error) {
output, err := findDurableExecution(ctx, conn, arn)

if tfresource.NotFound(err) {
return nil, "", nil
}

if err != nil {
return nil, "", err
}

return output, string(output.Status), nil
}
}

func waitDurableExecutionStopped(ctx context.Context, conn *lambda.Client, arn string, timeout time.Duration) (*lambda.GetDurableExecutionOutput, error) {
stateConf := &retry.StateChangeConf{
Pending: enum.Slice(awstypes.ExecutionStatusRunning),
Target: enum.Slice(awstypes.ExecutionStatusStopped),
Refresh: statusDurableExecution(ctx, conn, arn),
Timeout: timeout,
Delay: 2 * time.Second,
}

outputRaw, err := stateConf.WaitForStateContext(ctx)

if output, ok := outputRaw.(*lambda.GetDurableExecutionOutput); ok {
return output, err
}

return nil, err
}

func findFunctionByName(ctx context.Context, conn *lambda.Client, name string) (*lambda.GetFunctionOutput, error) {
input := lambda.GetFunctionInput{
FunctionName: aws.String(name),
Expand Down Expand Up @@ -1503,6 +1647,24 @@ func waitFunctionConfigurationUpdated(ctx context.Context, conn *lambda.Client,
return nil, err
}

func waitFunctionDeleted(ctx context.Context, conn *lambda.Client, name string, timeout time.Duration) (*lambda.GetFunctionOutput, error) {
stateConf := &retry.StateChangeConf{
Pending: enum.Slice(awstypes.StateActive, awstypes.StateActiveNonInvocable, awstypes.StatePending, awstypes.StateInactive, awstypes.StateFailed, awstypes.StateDeleting),
Target: []string{},
Refresh: statusFunctionState(ctx, conn, name),
Timeout: timeout,
Delay: 5 * time.Second,
}

outputRaw, err := stateConf.WaitForStateContext(ctx)

if output, ok := outputRaw.(*awstypes.FunctionConfiguration); ok {
return &lambda.GetFunctionOutput{Configuration: output}, err
}

return nil, err
}

// retryFunctionOp retries a Lambda Function Create or Update operation.
// It handles IAM eventual consistency and EC2 throttling.
type functionCU interface {
Expand Down Expand Up @@ -1627,6 +1789,7 @@ func needsFunctionConfigUpdate(d sdkv2.ResourceDiffer) bool {
d.HasChange(names.AttrKMSKeyARN) ||
d.HasChange("layers") ||
d.HasChange("dead_letter_config") ||
d.HasChange("durable_config") ||
d.HasChange("snap_start") ||
d.HasChange("tracing_config") ||
d.HasChange("vpc_config.0.ipv6_allowed_for_dual_stack") ||
Expand Down Expand Up @@ -1776,6 +1939,40 @@ func expandFileSystemConfigs(tfList []any) []awstypes.FileSystemConfig {
return apiObjects
}

func expandDurableConfigs(tfList []any) *awstypes.DurableConfig {
if len(tfList) == 0 || tfList[0] == nil {
return nil
}

tfMap := tfList[0].(map[string]any)
return &awstypes.DurableConfig{
ExecutionTimeout: aws.Int32(int32(tfMap["execution_timeout"].(int))),
RetentionPeriodInDays: aws.Int32(int32(tfMap[names.AttrRetentionPeriod].(int))),
}
}

func flattenDurableConfig(apiObject *awstypes.DurableConfig) []any {
if apiObject == nil {
return nil
}

tfMap := map[string]any{}

if v := apiObject.ExecutionTimeout; v != nil {
tfMap["execution_timeout"] = aws.ToInt32(v)
}

if v := apiObject.RetentionPeriodInDays; v != nil {
tfMap[names.AttrRetentionPeriod] = aws.ToInt32(v)
}

if len(tfMap) == 0 {
return nil
}

return []any{tfMap}
}

func flattenImageConfig(apiObject *awstypes.ImageConfigResponse) []any {
tfMap := make(map[string]any)

Expand Down
21 changes: 21 additions & 0 deletions internal/service/lambda/function_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@ func dataSourceFunction() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"durable_config": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"execution_timeout": {
Type: schema.TypeInt,
Computed: true,
},
names.AttrRetentionPeriod: {
Type: schema.TypeInt,
Computed: true,
},
},
},
},
names.AttrEnvironment: {
Type: schema.TypeList,
Computed: true,
Expand Down Expand Up @@ -370,6 +386,11 @@ func dataSourceFunctionRead(ctx context.Context, d *schema.ResourceData, meta an
d.Set("dead_letter_config", []any{})
}
d.Set(names.AttrDescription, function.Description)
if function.DurableConfig != nil {
if err := d.Set("durable_config", flattenDurableConfig(function.DurableConfig)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting durable_config: %s", err)
}
}
if err := d.Set(names.AttrEnvironment, flattenEnvironment(function.Environment)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting environment: %s", err)
}
Expand Down
43 changes: 43 additions & 0 deletions internal/service/lambda/function_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,30 @@ func TestAccLambdaFunctionDataSource_tenancyConfig(t *testing.T) {
})
}

func TestAccLambdaFunctionDataSource_durableConfig(t *testing.T) {
ctx := acctest.Context(t)
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
dataSourceName := "data.aws_lambda_function.test"
resourceName := "aws_lambda_function.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.LambdaServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccFunctionDataSourceConfig_durableConfig(rName),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrPair(dataSourceName, names.AttrARN, resourceName, names.AttrARN),
resource.TestCheckResourceAttrPair(dataSourceName, "durable_config.#", resourceName, "durable_config.#"),
resource.TestCheckResourceAttrPair(dataSourceName, "durable_config.0.execution_timeout", resourceName, "durable_config.0.execution_timeout"),
resource.TestCheckResourceAttrPair(dataSourceName, "durable_config.0.retention_period", resourceName, "durable_config.0.retention_period"),
),
},
},
})
}

func TestAccLambdaFunctionDataSource_capacityProvider(t *testing.T) {
ctx := acctest.Context(t)
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
Expand Down Expand Up @@ -944,3 +968,22 @@ data "aws_lambda_function" "test" {
}
`, rName))
}

func testAccFunctionDataSourceConfig_durableConfig(rName string) string {
return acctest.ConfigCompose(testAccFunctionDataSourceConfig_base(rName), fmt.Sprintf(`
resource "aws_lambda_function" "test" {
filename = "test-fixtures/lambdatest.zip"
function_name = %[1]q
handler = "exports.example"
role = aws_iam_role.lambda.arn
runtime = "nodejs22.x"
durable_config {
execution_timeout = 300
retention_period = 7
}
}
data "aws_lambda_function" "test" {
function_name = aws_lambda_function.test.function_name
}
`, rName))
}
Loading
Loading