Skip to content

Commit 70894c6

Browse files
authored
Merge pull request #324 from upbound/ddb-mrsc-streaming-v6.55.0
Propagate Streaming Configuration to DynamoDB MRSC Replicas
2 parents 4cf4659 + c2c7ba9 commit 70894c6

1 file changed

Lines changed: 205 additions & 0 deletions

File tree

internal/service/dynamodb/table.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
sdkid "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id"
2727
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
2828
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
29+
2930
"github.com/hashicorp/terraform-provider-aws/internal/conns"
3031
"github.com/hashicorp/terraform-provider-aws/internal/create"
3132
tfcty "github.com/hashicorp/terraform-provider-aws/internal/cty"
@@ -98,6 +99,7 @@ func resourceTable() *schema.Resource {
9899
}
99100
return nil
100101
},
102+
mrscReplicaTableDrift,
101103
customdiff.ForceNewIfChange("restore_source_name", func(_ context.Context, old, new, meta any) bool {
102104
// If they differ force new unless new is cleared
103105
// https://github.com/hashicorp/terraform-provider-aws/issues/25214
@@ -461,6 +463,16 @@ func resourceTable() *schema.Resource {
461463
Type: schema.TypeString,
462464
Computed: true,
463465
},
466+
// Per-replica stream state (from the replica table's
467+
// StreamSpecification), to detect MRSC replica stream drift.
468+
"stream_enabled": {
469+
Type: schema.TypeBool,
470+
Computed: true,
471+
},
472+
"stream_view_type": {
473+
Type: schema.TypeString,
474+
Computed: true,
475+
},
464476
},
465477
},
466478
},
@@ -1009,6 +1021,14 @@ func resourceTableCreate(ctx context.Context, d *schema.ResourceData, meta any)
10091021
if err := updateReplicaTags(ctx, conn, aws.ToString(output.TableArn), v.List(), keyValueTags(ctx, getTagsIn(ctx))); err != nil {
10101022
return create.AppendDiagError(diags, names.DynamoDB, create.ErrActionCreating, resNameTable, d.Id(), fmt.Errorf("replica tags: %w", err))
10111023
}
1024+
1025+
// Streaming configuration is not automatically propagated to MRSC replica
1026+
// tables, so propagate the configuration explicitly.
1027+
if d.Get("stream_enabled").(bool) {
1028+
if err := updateReplicaStreams(ctx, conn, d.Id(), v.List(), true, d.Get("stream_view_type").(string), d.Timeout(schema.TimeoutCreate)); err != nil {
1029+
return create.AppendDiagError(diags, names.DynamoDB, create.ErrActionCreating, resNameTable, d.Id(), fmt.Errorf("replica streams: %w", err))
1030+
}
1031+
}
10121032
}
10131033

10141034
return append(diags, resourceTableRead(ctx, d, meta)...)
@@ -1514,6 +1534,14 @@ func resourceTableUpdate(ctx context.Context, d *schema.ResourceData, meta any)
15141534
}
15151535
}
15161536

1537+
// Reconcile the DynamoDB Streams configuration of MRSC STRONG
1538+
// consistency-mode replicas to match the global-table level desired state.
1539+
if replicas := d.Get("replica").(*schema.Set); replicas.Len() > 0 {
1540+
if err := updateReplicaStreams(ctx, conn, d.Id(), replicas.List(), d.Get("stream_enabled").(bool), d.Get("stream_view_type").(string), d.Timeout(schema.TimeoutUpdate)); err != nil {
1541+
return create.AppendDiagError(diags, names.DynamoDB, create.ErrActionUpdating, resNameTable, d.Id(), err)
1542+
}
1543+
}
1544+
15171545
if d.HasChange("point_in_time_recovery") {
15181546
if err := updatePITR(ctx, conn, d.Id(), d.Get("point_in_time_recovery.0.enabled").(bool), aws.Int32(int32(d.Get("point_in_time_recovery.0.recovery_period_in_days").(int))), meta.(*conns.AWSClient).Region(ctx), d.Timeout(schema.TimeoutUpdate)); err != nil {
15191547
return create.AppendDiagError(diags, names.DynamoDB, create.ErrActionUpdating, resNameTable, d.Id(), err)
@@ -1614,6 +1642,136 @@ func cycleStreamEnabled(ctx context.Context, conn *dynamodb.Client, id string, s
16141642
return nil
16151643
}
16161644

1645+
// updateReplicaStreams propagates the global-table level streaming
1646+
// configuration to each MRSC replica of the table when the consistency mode
1647+
// is STRONG.
1648+
//
1649+
// MREC replicas are skipped as DynamoDB uses streams for replication with that
1650+
// consistency mode and hence streaming cannot be disabled/modified.
1651+
func updateReplicaStreams(ctx context.Context, conn *dynamodb.Client, tableName string, replicas []any, streamEnabled bool, streamViewType string, timeout time.Duration) error {
1652+
for _, tfMapRaw := range replicas {
1653+
tfMap, ok := tfMapRaw.(map[string]any)
1654+
if !ok {
1655+
continue
1656+
}
1657+
1658+
// Only MRSC (STRONG) replicas need explicit stream propagation.
1659+
if v, ok := tfMap["consistency_mode"].(string); !ok || awstypes.MultiRegionConsistency(v) != awstypes.MultiRegionConsistencyStrong {
1660+
continue
1661+
}
1662+
1663+
region, ok := tfMap["region_name"].(string)
1664+
if !ok || region == "" {
1665+
continue
1666+
}
1667+
1668+
curEnabled, curViewType, err := replicaStreamState(ctx, conn, tableName, region)
1669+
if err != nil {
1670+
return fmt.Errorf("reading stream state for replica (%s): %w", region, err)
1671+
}
1672+
1673+
switch {
1674+
case streamEnabled && !curEnabled:
1675+
// Replica table has streaming disabled, enable it.
1676+
if err := updateReplicaStreamSpecification(ctx, conn, tableName, region, &awstypes.StreamSpecification{
1677+
StreamEnabled: aws.Bool(true),
1678+
StreamViewType: awstypes.StreamViewType(streamViewType),
1679+
}, timeout); err != nil {
1680+
return fmt.Errorf("enabling stream for replica (%s): %w", region, err)
1681+
}
1682+
case streamEnabled && curEnabled && string(curViewType) != streamViewType:
1683+
// Replica table has streaming enabled but is configured with the wrong
1684+
// view type, reconcile the drift.
1685+
if err := cycleReplicaStreamEnabled(ctx, conn, tableName, region, awstypes.StreamViewType(streamViewType), timeout); err != nil {
1686+
return fmt.Errorf("changing stream view type for replica (%s): %w", region, err)
1687+
}
1688+
case !streamEnabled && curEnabled:
1689+
// Replica table has streaming enabled, but in the desired state,
1690+
// it should be disabled.
1691+
if err := updateReplicaStreamSpecification(ctx, conn, tableName, region, &awstypes.StreamSpecification{
1692+
StreamEnabled: aws.Bool(false),
1693+
}, timeout); err != nil {
1694+
return fmt.Errorf("disabling stream for replica (%s): %w", region, err)
1695+
}
1696+
}
1697+
// Else, replica table is already in the desired state.
1698+
}
1699+
1700+
return nil
1701+
}
1702+
1703+
// replicaStreamState returns the current DynamoDB streaming configuration
1704+
// for the specified replica table in the specified region.
1705+
// It returns whether streaming is actually enabled, and if it's enabled,
1706+
// the configured streaming view type for the replica table.
1707+
func replicaStreamState(ctx context.Context, conn *dynamodb.Client, tableName, region string) (bool, awstypes.StreamViewType, error) {
1708+
table, err := findTableByName(ctx, conn, tableName, func(o *dynamodb.Options) { o.Region = region })
1709+
if err != nil {
1710+
return false, "", err
1711+
}
1712+
if table.StreamSpecification == nil || !aws.ToBool(table.StreamSpecification.StreamEnabled) {
1713+
return false, "", nil
1714+
}
1715+
return true, table.StreamSpecification.StreamViewType, nil
1716+
}
1717+
1718+
// cycleReplicaStreamEnabled disables and then re-enables the stream on a replica Region
1719+
// with streamViewType. It mirrors cycleStreamEnabled for the primary Region and is
1720+
// required to change the stream view type of an already-enabled MRSC replica stream.
1721+
func cycleReplicaStreamEnabled(ctx context.Context, conn *dynamodb.Client, tableName, region string, streamViewType awstypes.StreamViewType, timeout time.Duration) error {
1722+
if err := updateReplicaStreamSpecification(ctx, conn, tableName, region, &awstypes.StreamSpecification{
1723+
StreamEnabled: aws.Bool(false),
1724+
}, timeout); err != nil {
1725+
return fmt.Errorf("disabling stream: %w", err)
1726+
}
1727+
1728+
if err := updateReplicaStreamSpecification(ctx, conn, tableName, region, &awstypes.StreamSpecification{
1729+
StreamEnabled: aws.Bool(true),
1730+
StreamViewType: streamViewType,
1731+
}, timeout); err != nil {
1732+
return fmt.Errorf("re-enabling stream: %w", err)
1733+
}
1734+
1735+
return nil
1736+
}
1737+
1738+
// updateReplicaStreamSpecification issues a regional UpdateTable to set the stream
1739+
// specification on a single replica region and waits for that replica to become active.
1740+
//
1741+
// Global-table control-plane operations (e.g. an MRSC group still stabilizing after
1742+
// replica creation) can briefly leave the table in the UPDATING state, during which a
1743+
// further UpdateTable is rejected with ResourceInUseException. That error (and throttling)
1744+
// is therefore retried here, mirroring how createReplicas issues its UpdateTable calls.
1745+
func updateReplicaStreamSpecification(ctx context.Context, conn *dynamodb.Client, tableName, region string, streamSpec *awstypes.StreamSpecification, timeout time.Duration) error {
1746+
input := &dynamodb.UpdateTableInput{
1747+
TableName: aws.String(tableName),
1748+
StreamSpecification: streamSpec,
1749+
}
1750+
optFn := func(o *dynamodb.Options) { o.Region = region }
1751+
1752+
err := tfresource.Retry(ctx, max(replicaUpdateTimeout, timeout), func(ctx context.Context) *tfresource.RetryError {
1753+
if _, err := conn.UpdateTable(ctx, input, optFn); err != nil {
1754+
if tfawserr.ErrCodeEquals(err, errCodeThrottlingException) {
1755+
return tfresource.RetryableError(err)
1756+
}
1757+
if errs.IsA[*awstypes.ResourceInUseException](err) {
1758+
return tfresource.RetryableError(err)
1759+
}
1760+
return tfresource.NonRetryableError(err)
1761+
}
1762+
return nil
1763+
})
1764+
if err != nil {
1765+
return err
1766+
}
1767+
1768+
if _, err := waitReplicaActive(ctx, conn, tableName, region, timeout, replicaPropagationDelay); err != nil {
1769+
return err
1770+
}
1771+
1772+
return nil
1773+
}
1774+
16171775
func createReplicas(ctx context.Context, conn *dynamodb.Client, tableName string, tfList []any, globalTableWitnessRegionName string, create bool, timeout time.Duration) error {
16181776
// Duplicating this for MRSC Adoption. If using MRSC and CreateReplicationGroupMemberAction list isn't initiated for at least 2 replicas
16191777
// then the update table action will fail with
@@ -2622,6 +2780,15 @@ func enrichReplicas(ctx context.Context, conn *dynamodb.Client, arn, tableName s
26222780
}
26232781
tfMap[names.AttrStreamARN] = aws.ToString(table.LatestStreamArn)
26242782
tfMap["stream_label"] = aws.ToString(table.LatestStreamLabel)
2783+
// The streaming configuration for the replica table
2784+
// (whether streaming is enabled and the streaming view type).
2785+
if table.StreamSpecification != nil {
2786+
tfMap["stream_enabled"] = aws.ToBool(table.StreamSpecification.StreamEnabled)
2787+
tfMap["stream_view_type"] = string(table.StreamSpecification.StreamViewType)
2788+
} else {
2789+
tfMap["stream_enabled"] = false
2790+
tfMap["stream_view_type"] = ""
2791+
}
26252792

26262793
tfList[i] = tfMap
26272794
}
@@ -3835,3 +4002,41 @@ func ctyValueLegacyEquals(lhs, rhs cty.Value) bool {
38354002

38364003
return false
38374004
}
4005+
4006+
// mrscReplicaTableDrift detects MRSC replica stream drift when the consistency
4007+
// mode is STRONG. Each replica table's actual streaming state is stored in the
4008+
// stream_enabled and stream_view_type computed attributes in enrichReplicas,
4009+
// and if any replica table's streaming configuration does not match the
4010+
// global-table level configuration, a diff is enforced.
4011+
func mrscReplicaTableDrift(_ context.Context, diff *schema.ResourceDiff, _ any) error {
4012+
if diff.Id() == "" {
4013+
return nil
4014+
}
4015+
4016+
oldReplicas, _ := diff.GetChange("replica")
4017+
replicas, ok := oldReplicas.(*schema.Set)
4018+
if !ok {
4019+
return nil
4020+
}
4021+
4022+
wantEnabled, _ := diff.Get("stream_enabled").(bool)
4023+
wantViewType, _ := diff.Get("stream_view_type").(string)
4024+
for _, tfMapRaw := range replicas.List() {
4025+
tfMap, ok := tfMapRaw.(map[string]any)
4026+
if !ok {
4027+
continue
4028+
}
4029+
if v, _ := tfMap["consistency_mode"].(string); awstypes.MultiRegionConsistency(v) != awstypes.MultiRegionConsistencyStrong {
4030+
continue
4031+
}
4032+
gotEnabled, _ := tfMap["stream_enabled"].(bool)
4033+
gotViewType, _ := tfMap["stream_view_type"].(string)
4034+
if gotEnabled != wantEnabled || (wantEnabled && gotEnabled && gotViewType != wantViewType) {
4035+
if err := diff.SetNewComputed(names.AttrStreamARN); err != nil {
4036+
return fmt.Errorf("forcing diff for MRSC replica streaming configuration drift: %w", err)
4037+
}
4038+
break
4039+
}
4040+
}
4041+
return nil
4042+
}

0 commit comments

Comments
 (0)