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
3 changes: 3 additions & 0 deletions .changelog/48464.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_neptune_cluster: Add support for `restore_to_point_in_time` configuration block
```
113 changes: 106 additions & 7 deletions internal/service/neptune/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Expand All @@ -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 {
Expand All @@ -410,19 +487,22 @@ 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 {
v := v.(string)

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)
Expand All @@ -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 {
Expand All @@ -459,13 +540,15 @@ 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 {
v := v.(*schema.Set)

inputC.VpcSecurityGroupIds = flex.ExpandStringValueSet(v)
inputR.VpcSecurityGroupIds = flex.ExpandStringValueSet(v)
inputPITR.VpcSecurityGroupIds = flex.ExpandStringValueSet(v)
if restoreDBClusterFromSnapshot {
clusterUpdate = true
inputM.VpcSecurityGroupIds = flex.ExpandStringValueSet(v)
Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)
Expand Down
104 changes: 104 additions & 0 deletions internal/service/neptune/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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" {
Expand Down
26 changes: 25 additions & 1 deletion website/docs/r/neptune_cluster.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down