diff --git a/.changelog/48464.txt b/.changelog/48464.txt new file mode 100644 index 000000000000..e0f104894a83 --- /dev/null +++ b/.changelog/48464.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_neptune_cluster: Add support for `restore_to_point_in_time` configuration block +``` diff --git a/internal/service/neptune/cluster.go b/internal/service/neptune/cluster.go index 53513af16832..90a39d59c2ae 100644 --- a/internal/service/neptune/cluster.go +++ b/internal/service/neptune/cluster.go @@ -252,6 +252,50 @@ func resourceCluster() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "restore_to_point_in_time": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "restore_to_time": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: verify.ValidUTCTimestamp, + ExactlyOneOf: []string{ + "restore_to_point_in_time.0.restore_to_time", + "restore_to_point_in_time.0.use_latest_restorable_time", + }, + }, + "restore_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{"full-copy", "copy-on-write"}, false), + }, + "source_cluster_identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validIdentifier, + }, + "use_latest_restorable_time": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + ExactlyOneOf: []string{ + "restore_to_point_in_time.0.restore_to_time", + "restore_to_point_in_time.0.use_latest_restorable_time", + }, + }, + }, + }, + ConflictsWith: []string{ + "snapshot_identifier", + }, + }, "serverless_v2_scaling_configuration": { Type: schema.TypeList, Optional: true, @@ -290,6 +334,7 @@ func resourceCluster() *schema.Resource { // allow snapshot_idenfitier to be removed without forcing re-creation return new == "" }, + ConflictsWith: []string{"restore_to_point_in_time"}, }, names.AttrStorageEncrypted: { Type: schema.TypeBool, @@ -337,7 +382,11 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta any // Check if any of the parameters that require a cluster modification after creation are set. // See https://docs.aws.amazon.com/neptune/latest/userguide/backup-restore-restore-snapshot.html#backup-restore-restore-snapshot-considerations. clusterUpdate := false + restoreToPointInTime := false restoreDBClusterFromSnapshot := false + if _, ok := d.GetOk("restore_to_point_in_time"); ok { + restoreToPointInTime = true + } if _, ok := d.GetOk("snapshot_identifier"); ok { restoreDBClusterFromSnapshot = true } @@ -367,6 +416,33 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta any ApplyImmediately: aws.Bool(true), DBClusterIdentifier: aws.String(clusterID), } + inputPITR := &neptune.RestoreDBClusterToPointInTimeInput{ + DBClusterIdentifier: aws.String(clusterID), + DeletionProtection: aws.Bool(d.Get(names.AttrDeletionProtection).(bool)), + Port: aws.Int32(int32(d.Get(names.AttrPort).(int))), + ServerlessV2ScalingConfiguration: serverlessConfiguration, + Tags: getTagsIn(ctx), + } + + if restoreToPointInTime { + v, _ := d.GetOk("restore_to_point_in_time") + tfMap := v.([]any)[0].(map[string]any) + + inputPITR.SourceDBClusterIdentifier = aws.String(tfMap["source_cluster_identifier"].(string)) + + if v, ok := tfMap["restore_type"].(string); ok && v != "" { + inputPITR.RestoreType = aws.String(v) + } + + if v, ok := tfMap["restore_to_time"].(string); ok && v != "" { + t, _ := time.Parse(time.RFC3339, v) + inputPITR.RestoreToTime = aws.Time(t) + } + + if v, ok := tfMap["use_latest_restorable_time"].(bool); ok && v { + inputPITR.UseLatestRestorableTime = aws.Bool(v) + } + } if v, ok := d.GetOk(names.AttrAvailabilityZones); ok && v.(*schema.Set).Len() > 0 { v := v.(*schema.Set) @@ -379,7 +455,7 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta any v := int32(v.(int)) inputC.BackupRetentionPeriod = aws.Int32(v) - if restoreDBClusterFromSnapshot { + if restoreDBClusterFromSnapshot || restoreToPointInTime { clusterUpdate = true inputM.BackupRetentionPeriod = aws.Int32(v) } @@ -390,6 +466,7 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta any inputC.EnableCloudwatchLogsExports = flex.ExpandStringValueSet(v) inputR.EnableCloudwatchLogsExports = flex.ExpandStringValueSet(v) + inputPITR.EnableCloudwatchLogsExports = flex.ExpandStringValueSet(v) } if v, ok := d.GetOk(names.AttrEngineVersion); ok { @@ -410,6 +487,7 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta any inputC.EnableIAMDatabaseAuthentication = aws.Bool(v) inputR.EnableIAMDatabaseAuthentication = aws.Bool(v) + inputPITR.EnableIAMDatabaseAuthentication = aws.Bool(v) } if v, ok := d.GetOk(names.AttrKMSKeyARN); ok { @@ -417,12 +495,14 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta any inputC.KmsKeyId = aws.String(v) inputR.KmsKeyId = aws.String(v) + inputPITR.KmsKeyId = aws.String(v) } if v, ok := d.GetOk("neptune_cluster_parameter_group_name"); ok { v := v.(string) inputC.DBClusterParameterGroupName = aws.String(v) + inputPITR.DBClusterParameterGroupName = aws.String(v) if restoreDBClusterFromSnapshot { clusterUpdate = true inputM.DBClusterParameterGroupName = aws.String(v) @@ -434,6 +514,7 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta any inputC.DBSubnetGroupName = aws.String(v) inputR.DBSubnetGroupName = aws.String(v) + inputPITR.DBSubnetGroupName = aws.String(v) } if v, ok := d.GetOk("preferred_backup_window"); ok { @@ -459,6 +540,7 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta any inputC.StorageType = aws.String(v) inputR.StorageType = aws.String(v) + inputPITR.StorageType = aws.String(v) } if v, ok := d.GetOk(names.AttrVPCSecurityGroupIDs); ok && v.(*schema.Set).Len() > 0 { @@ -466,6 +548,7 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta any inputC.VpcSecurityGroupIds = flex.ExpandStringValueSet(v) inputR.VpcSecurityGroupIds = flex.ExpandStringValueSet(v) + inputPITR.VpcSecurityGroupIds = flex.ExpandStringValueSet(v) if restoreDBClusterFromSnapshot { clusterUpdate = true inputM.VpcSecurityGroupIds = flex.ExpandStringValueSet(v) @@ -474,7 +557,21 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta any var err error - if restoreDBClusterFromSnapshot { + if restoreToPointInTime { + for l := backoff.NewLoop(propagationTimeout); l.Continue(ctx); { + _, err = conn.RestoreDBClusterToPointInTime(ctx, inputPITR) + + if tfawserr.ErrMessageContains(err, errCodeInvalidParameterValue, "IAM role ARN value is invalid") { + continue + } + + break + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "creating Neptune Cluster (restore to point-in-time) (%s): %s", clusterID, err) + } + } else if restoreDBClusterFromSnapshot { for l := backoff.NewLoop(propagationTimeout); l.Continue(ctx); { _, err = conn.RestoreDBClusterFromSnapshot(ctx, inputR) @@ -484,9 +581,11 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta any break } - } - if !restoreDBClusterFromSnapshot { + if err != nil { + return sdkdiag.AppendErrorf(diags, "creating Neptune Cluster (restore from snapshot) (%s): %s", clusterID, err) + } + } else { for l := backoff.NewLoop(d.Timeout(schema.TimeoutCreate)); l.Continue(ctx); { _, err = conn.CreateDBCluster(ctx, inputC) @@ -500,10 +599,10 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta any break } - } - if err != nil { - return sdkdiag.AppendErrorf(diags, "creating Neptune Cluster (%s): %s", clusterID, err) + if err != nil { + return sdkdiag.AppendErrorf(diags, "creating Neptune Cluster (%s): %s", clusterID, err) + } } d.SetId(clusterID) diff --git a/internal/service/neptune/cluster_test.go b/internal/service/neptune/cluster_test.go index 6dbf161205cd..6b0fae53acde 100644 --- a/internal/service/neptune/cluster_test.go +++ b/internal/service/neptune/cluster_test.go @@ -39,6 +39,7 @@ func testAccClusterImportStep(n string) resource.TestStep { names.AttrFinalSnapshotIdentifier, "cluster_members", "neptune_instance_parameter_group_name", + "restore_to_point_in_time", "skip_final_snapshot", "snapshot_identifier", "cluster_members", @@ -952,6 +953,54 @@ func TestAccNeptuneCluster_restoreFromSnapshot(t *testing.T) { }) } +func TestAccNeptuneCluster_restoreToPointInTime_latestRestorableTime(t *testing.T) { + ctx := acctest.Context(t) + var dbCluster awstypes.DBCluster + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_neptune_cluster.test" + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NeptuneServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckClusterDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccClusterConfig_restoreToPointInTime_latestRestorableTime(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckClusterExists(ctx, resourceName, &dbCluster), + resource.TestCheckResourceAttr(resourceName, names.AttrClusterIdentifier, rName), + ), + }, + testAccClusterImportStep(resourceName), + }, + }) +} + +func TestAccNeptuneCluster_restoreToPointInTime_copyOnWrite(t *testing.T) { + ctx := acctest.Context(t) + var dbCluster awstypes.DBCluster + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_neptune_cluster.test" + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NeptuneServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckClusterDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccClusterConfig_restoreToPointInTime_copyOnWrite(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckClusterExists(ctx, resourceName, &dbCluster), + resource.TestCheckResourceAttr(resourceName, names.AttrClusterIdentifier, rName), + ), + }, + testAccClusterImportStep(resourceName), + }, + }) +} + func TestAccNeptuneCluster_storageType(t *testing.T) { ctx := acctest.Context(t) var v awstypes.DBCluster @@ -1945,6 +1994,61 @@ resource "aws_neptune_cluster" "test" { `, rName) } +func testAccClusterConfig_restoreToPointInTime_latestRestorableTime(rName string) string { + return fmt.Sprintf(` +resource "aws_neptune_cluster" "source" { + cluster_identifier = "%[1]s-src" + skip_final_snapshot = true +} + +resource "aws_neptune_cluster_instance" "source" { + cluster_identifier = aws_neptune_cluster.source.id + instance_class = "db.r5.large" + identifier = "%[1]s-src-inst" +} + +resource "aws_neptune_cluster" "test" { + cluster_identifier = %[1]q + skip_final_snapshot = true + + restore_to_point_in_time { + source_cluster_identifier = aws_neptune_cluster.source.cluster_identifier + use_latest_restorable_time = true + } + + depends_on = [aws_neptune_cluster_instance.source] +} +`, rName) +} + +func testAccClusterConfig_restoreToPointInTime_copyOnWrite(rName string) string { + return fmt.Sprintf(` +resource "aws_neptune_cluster" "source" { + cluster_identifier = "%[1]s-src" + skip_final_snapshot = true +} + +resource "aws_neptune_cluster_instance" "source" { + cluster_identifier = aws_neptune_cluster.source.id + instance_class = "db.r5.large" + identifier = "%[1]s-src-inst" +} + +resource "aws_neptune_cluster" "test" { + cluster_identifier = %[1]q + skip_final_snapshot = true + + restore_to_point_in_time { + source_cluster_identifier = aws_neptune_cluster.source.cluster_identifier + restore_type = "copy-on-write" + use_latest_restorable_time = true + } + + depends_on = [aws_neptune_cluster_instance.source] +} +`, rName) +} + func testAccClusterConfig_storageType(rName, storageType string) string { return acctest.ConfigCompose(testAccClusterConfig_base(), fmt.Sprintf(` resource "aws_neptune_cluster" "test" { diff --git a/website/docs/r/neptune_cluster.html.markdown b/website/docs/r/neptune_cluster.html.markdown index 30ecd045fee9..0f27fdc81c4c 100644 --- a/website/docs/r/neptune_cluster.html.markdown +++ b/website/docs/r/neptune_cluster.html.markdown @@ -65,7 +65,8 @@ This resource supports the following arguments: * `replication_source_identifier` - (Optional) ARN of a source Neptune cluster or Neptune instance if this Neptune cluster is to be created as a Read Replica. * `serverless_v2_scaling_configuration` - (Optional) If set, create the Neptune cluster as a serverless one. See [Serverless](#serverless) for example block attributes. * `skip_final_snapshot` - (Optional) Whether a final Neptune snapshot is created before the Neptune cluster is deleted. If true is specified, no Neptune snapshot is created. If false is specified, a Neptune snapshot is created before the Neptune cluster is deleted, using the value from `final_snapshot_identifier`. Default is `false`. -* `snapshot_identifier` - (Optional) Whether or not to create this cluster from a snapshot. You can use either the name or ARN when specifying a Neptune cluster snapshot, or the ARN when specifying a Neptune snapshot. Automated snapshots **should not** be used for this attribute, unless from a different cluster. Automated snapshots are deleted as part of cluster destruction when the resource is replaced. +* `restore_to_point_in_time` - (Optional, Forces new resource) A configuration block for restoring a Neptune cluster to an arbitrary point in time. Conflicts with `snapshot_identifier`. Requires at least one of `restore_to_time` or `use_latest_restorable_time`. [Detailed below](#restore_to_point_in_time). +* `snapshot_identifier` - (Optional) Whether or not to create this cluster from a snapshot. You can use either the name or ARN when specifying a Neptune cluster snapshot, or the ARN when specifying a Neptune snapshot. Automated snapshots **should not** be used for this attribute, unless from a different cluster. Automated snapshots are deleted as part of cluster destruction when the resource is replaced. Conflicts with `restore_to_point_in_time`. * `storage_encrypted` - (Optional) Whether the Neptune cluster is encrypted. The default is `false` if not specified. * `storage_type` - (Optional) Storage type associated with the cluster `standard/iopt1`. Default: `standard`. * `tags` - (Optional) Map of tags to assign to the Neptune cluster. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. @@ -99,6 +100,29 @@ resource "aws_neptune_cluster_instance" "example" { * `min_capacity`: (default: **2.5**) Minimum Neptune Capacity Units (NCUs) for this cluster. Must be greater or equal than **1**. See [AWS Documentation](https://docs.aws.amazon.com/neptune/latest/userguide/neptune-serverless-capacity-scaling.html) for more details. * `max_capacity`: (default: **128**) Maximum Neptune Capacity Units (NCUs) for this cluster. Must be lower or equal than **128**. See [AWS Documentation](https://docs.aws.amazon.com/neptune/latest/userguide/neptune-serverless-capacity-scaling.html) for more details. +### Restore To Point In Time + +```terraform +resource "aws_neptune_cluster" "example" { + cluster_identifier = "example" + skip_final_snapshot = true + + restore_to_point_in_time { + source_cluster_identifier = "example-source" + use_latest_restorable_time = true + } +} +``` + +### restore_to_point_in_time + +~> **NOTE:** Removing this configuration on the next apply will recreate the cluster. + +* `source_cluster_identifier` - (Required) The identifier of the source Neptune cluster from which to restore. +* `restore_type` - (Optional) The type of restore to be performed. Valid values are `full-copy` and `copy-on-write`. Default is `full-copy`. +* `restore_to_time` - (Optional) The date and time to restore from. Must be in UTC format (e.g., `2024-01-01T00:00:00Z`). Conflicts with `use_latest_restorable_time`. +* `use_latest_restorable_time` - (Optional) Set to `true` to restore the cluster to the latest restorable backup time. Conflicts with `restore_to_time`. + ## Attribute Reference This resource exports the following attributes in addition to the arguments above: