Skip to content

[Enhancement] aws_cloudwatch_log_group: Add support for log_group_class = "DELIVERY" with retention policy handling #42658

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

Merged
3 changes: 3 additions & 0 deletions .changelog/42658.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_cloudwatch_log_group: Support `DELIVERY` as a valid value for `log_group_class`
```
44 changes: 28 additions & 16 deletions internal/service/logs/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ func resourceGroup() *schema.Resource {
Optional: true,
Default: 0,
ValidateFunc: validation.IntInSlice([]int{0, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653}),
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
if d.HasChange("log_group_class") {
return false
}
if v, ok := d.GetOk("log_group_class"); ok {
if awstypes.LogGroupClass(v.(string)) == awstypes.LogGroupClassDelivery {
return true
}
}
return false
},
},
names.AttrSkipDestroy: {
Type: schema.TypeBool,
Expand All @@ -95,7 +106,7 @@ func resourceGroupCreate(ctx context.Context, d *schema.ResourceData, meta any)
conn := meta.(*conns.AWSClient).LogsClient(ctx)

name := create.Name(d.Get(names.AttrName).(string), d.Get(names.AttrNamePrefix).(string))
input := &cloudwatchlogs.CreateLogGroupInput{
input := cloudwatchlogs.CreateLogGroupInput{
LogGroupClass: awstypes.LogGroupClass(d.Get("log_group_class").(string)),
LogGroupName: aws.String(name),
Tags: getTagsIn(ctx),
Expand All @@ -105,22 +116,22 @@ func resourceGroupCreate(ctx context.Context, d *schema.ResourceData, meta any)
input.KmsKeyId = aws.String(v.(string))
}

_, err := conn.CreateLogGroup(ctx, input)
_, err := conn.CreateLogGroup(ctx, &input)

if err != nil {
return sdkdiag.AppendErrorf(diags, "creating CloudWatch Logs Log Group (%s): %s", name, err)
}

d.SetId(name)

if v, ok := d.GetOk("retention_in_days"); ok {
input := &cloudwatchlogs.PutRetentionPolicyInput{
if v, ok := d.GetOk("retention_in_days"); ok && input.LogGroupClass != awstypes.LogGroupClassDelivery {
input := cloudwatchlogs.PutRetentionPolicyInput{
LogGroupName: aws.String(d.Id()),
RetentionInDays: aws.Int32(int32(v.(int))),
}

_, err := tfresource.RetryWhenAWSErrMessageContains(ctx, propagationTimeout, func() (any, error) {
return conn.PutRetentionPolicy(ctx, input)
return conn.PutRetentionPolicy(ctx, &input)
}, "AccessDeniedException", "no identity-based policy allows the logs:PutRetentionPolicy action")

if err != nil {
Expand Down Expand Up @@ -165,24 +176,24 @@ func resourceGroupUpdate(ctx context.Context, d *schema.ResourceData, meta any)

if d.HasChange("retention_in_days") {
if v, ok := d.GetOk("retention_in_days"); ok {
input := &cloudwatchlogs.PutRetentionPolicyInput{
input := cloudwatchlogs.PutRetentionPolicyInput{
LogGroupName: aws.String(d.Id()),
RetentionInDays: aws.Int32(int32(v.(int))),
}

_, err := tfresource.RetryWhenAWSErrMessageContains(ctx, propagationTimeout, func() (any, error) {
return conn.PutRetentionPolicy(ctx, input)
return conn.PutRetentionPolicy(ctx, &input)
}, "AccessDeniedException", "no identity-based policy allows the logs:PutRetentionPolicy action")

if err != nil {
return sdkdiag.AppendErrorf(diags, "setting CloudWatch Logs Log Group (%s) retention policy: %s", d.Id(), err)
}
} else {
input := &cloudwatchlogs.DeleteRetentionPolicyInput{
input := cloudwatchlogs.DeleteRetentionPolicyInput{
LogGroupName: aws.String(d.Id()),
}

_, err := conn.DeleteRetentionPolicy(ctx, input)
_, err := conn.DeleteRetentionPolicy(ctx, &input)

if err != nil {
return sdkdiag.AppendErrorf(diags, "deleting CloudWatch Logs Log Group (%s) retention policy: %s", d.Id(), err)
Expand All @@ -192,22 +203,22 @@ func resourceGroupUpdate(ctx context.Context, d *schema.ResourceData, meta any)

if d.HasChange(names.AttrKMSKeyID) {
if v, ok := d.GetOk(names.AttrKMSKeyID); ok {
input := &cloudwatchlogs.AssociateKmsKeyInput{
input := cloudwatchlogs.AssociateKmsKeyInput{
KmsKeyId: aws.String(v.(string)),
LogGroupName: aws.String(d.Id()),
}

_, err := conn.AssociateKmsKey(ctx, input)
_, err := conn.AssociateKmsKey(ctx, &input)

if err != nil {
return sdkdiag.AppendErrorf(diags, "associating CloudWatch Logs Log Group (%s) KMS key: %s", d.Id(), err)
}
} else {
input := &cloudwatchlogs.DisassociateKmsKeyInput{
input := cloudwatchlogs.DisassociateKmsKeyInput{
LogGroupName: aws.String(d.Id()),
}

_, err := conn.DisassociateKmsKey(ctx, input)
_, err := conn.DisassociateKmsKey(ctx, &input)

if err != nil {
return sdkdiag.AppendErrorf(diags, "disassociating CloudWatch Logs Log Group (%s) KMS key: %s", d.Id(), err)
Expand All @@ -228,10 +239,11 @@ func resourceGroupDelete(ctx context.Context, d *schema.ResourceData, meta any)
}

log.Printf("[INFO] Deleting CloudWatch Logs Log Group: %s", d.Id())
input := cloudwatchlogs.DeleteLogGroupInput{
LogGroupName: aws.String(d.Id()),
}
_, err := tfresource.RetryWhenIsAErrorMessageContains[*awstypes.OperationAbortedException](ctx, 1*time.Minute, func() (any, error) {
return conn.DeleteLogGroup(ctx, &cloudwatchlogs.DeleteLogGroupInput{
LogGroupName: aws.String(d.Id()),
})
return conn.DeleteLogGroup(ctx, &input)
}, "try again")

if errs.IsA[*awstypes.ResourceNotFoundException](err) {
Expand Down
87 changes: 87 additions & 0 deletions internal/service/logs/group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/hashicorp/aws-sdk-go-base/v2/endpoints"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/id"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
tflogs "github.com/hashicorp/terraform-provider-aws/internal/service/logs"
Expand Down Expand Up @@ -331,6 +332,73 @@ func TestAccLogsGroup_skipDestroyInconsistentPlan(t *testing.T) {
})
}

// Test whether the log group is successfully created with the DELIVERY log group class when retention_in_days is set.
// Even if retention_in_days is changed in the configuration, the diff should be suppressed and the plan should be empty.
func TestAccLogsGroup_logGroupClassDELIVERY1(t *testing.T) {
ctx := acctest.Context(t)
var v types.LogGroup
rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix)
resourceName := "aws_cloudwatch_log_group.test"

acctest.ParallelTest(ctx, t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.LogsServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckLogGroupDestroy(ctx, t),
Steps: []resource.TestStep{
{
Config: testAccGroupConfig_logGroupClassDEVIVERYWithRetentionInDays(rName, 30),
Check: resource.ComposeTestCheckFunc(
testAccCheckLogGroupExists(ctx, t, resourceName, &v),
// AWS API forces retention_in_days to 2 for DELIVERY log group class
resource.TestCheckResourceAttr(resourceName, "retention_in_days", "2"),
),
},
{
Config: testAccGroupConfig_logGroupClassDEVIVERYWithRetentionInDays(rName, 60),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
{
Config: testAccGroupConfig_logGroupClassDEVIVERYWithoutRetentionInDays(rName),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
},
})
}

// Test whether the log group is successfully created with the DELIVERY log group class when retention_in_days is not set.
func TestAccLogsGroup_logGroupClassDELIVERY2(t *testing.T) {
ctx := acctest.Context(t)
var v types.LogGroup
rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix)
resourceName := "aws_cloudwatch_log_group.test"

acctest.ParallelTest(ctx, t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ErrorCheck: acctest.ErrorCheck(t, names.LogsServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckLogGroupDestroy(ctx, t),
Steps: []resource.TestStep{
{
Config: testAccGroupConfig_logGroupClassDEVIVERYWithoutRetentionInDays(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckLogGroupExists(ctx, t, resourceName, &v),
// AWS API forces retention_in_days to 2 for DELIVERY log group class
resource.TestCheckResourceAttr(resourceName, "retention_in_days", "2"),
),
},
},
})
}

func testAccCheckLogGroupExists(ctx context.Context, t *testing.T, n string, v *types.LogGroup) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
Expand Down Expand Up @@ -484,3 +552,22 @@ resource "aws_cloudwatch_log_group" "test" {
}
`, rName)
}

func testAccGroupConfig_logGroupClassDEVIVERYWithRetentionInDays(rName string, retentionInDays int) string {
return fmt.Sprintf(`
resource "aws_cloudwatch_log_group" "test" {
name = %[1]q
log_group_class = "DELIVERY"
retention_in_days = %[2]d
}
`, rName, retentionInDays)
}

func testAccGroupConfig_logGroupClassDEVIVERYWithoutRetentionInDays(rName string) string {
return fmt.Sprintf(`
resource "aws_cloudwatch_log_group" "test" {
name = %[1]q
log_group_class = "DELIVERY"
}
`, rName)
}
4 changes: 2 additions & 2 deletions website/docs/r/cloudwatch_log_group.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ This resource supports the following arguments:
* `name` - (Optional, Forces new resource) The name of the log group. If omitted, Terraform will assign a random, unique name.
* `name_prefix` - (Optional, Forces new resource) Creates a unique name beginning with the specified prefix. Conflicts with `name`.
* `skip_destroy` - (Optional) Set to true if you do not wish the log group (and any logs it may contain) to be deleted at destroy time, and instead just remove the log group from the Terraform state.
* `log_group_class` - (Optional) Specified the log class of the log group. Possible values are: `STANDARD` or `INFREQUENT_ACCESS`.
* `log_group_class` - (Optional) Specified the log class of the log group. Possible values are: `STANDARD`, `INFREQUENT_ACCESS`, or `DELIVERY`.
* `retention_in_days` - (Optional) Specifies the number of days
you want to retain log events in the specified log group. Possible values are: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653, and 0.
If you select 0, the events in the log group are always retained and never expire.
If you select 0, the events in the log group are always retained and never expire. If `log_group_class` is set to `DELIVERY`, this argument is ignored and `retention_in_days` is forcibly set to 2.
* `kms_key_id` - (Optional) The ARN of the KMS Key to use when encrypting log data. Please note, after the AWS KMS CMK is disassociated from the log group,
AWS CloudWatch Logs stops encrypting newly ingested data for the log group. All previously ingested data remains encrypted, and AWS CloudWatch Logs requires
permissions for the CMK whenever the encrypted data is requested.
Expand Down
Loading