Skip to content

Commit beec4fe

Browse files
authored
Merge pull request #974 from crossplane/backport-973-to-release-0.21
[Backport release-0.21] Accomodate DynamoDB API's implied defaults
2 parents 289ef75 + 1e5fbce commit beec4fe

4 files changed

Lines changed: 285 additions & 76 deletions

File tree

examples/dynamodb/table.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ spec:
1111
keySchema:
1212
- attributeName: attribute1
1313
keyType: HASH
14+
billingMode: PROVISIONED
1415
provisionedThroughput:
1516
readCapacityUnits: 1
1617
writeCapacityUnits: 1
@@ -25,6 +26,32 @@ spec:
2526
---
2627
apiVersion: dynamodb.aws.crossplane.io/v1alpha1
2728
kind: Table
29+
metadata:
30+
name: sample-table-ppr
31+
spec:
32+
forProvider:
33+
region: us-east-1
34+
attributeDefinitions:
35+
- attributeName: attribute1
36+
attributeType: S
37+
keySchema:
38+
- attributeName: attribute1
39+
keyType: HASH
40+
# Note that due to a quirk of the DynamoDB API if you change billing mode
41+
# from PROVISIONED to PAY_PER_REQUEST you must also set readCapacityUnits
42+
# and writeCapacityUnits to 0.
43+
billingMode: PAY_PER_REQUEST
44+
streamSpecification:
45+
streamEnabled: true
46+
streamViewType: NEW_AND_OLD_IMAGES
47+
providerConfigRef:
48+
name: example
49+
writeConnectionSecretToRef:
50+
name: sample-table-ppr
51+
namespace: crossplane-system
52+
---
53+
apiVersion: dynamodb.aws.crossplane.io/v1alpha1
54+
kind: Table
2855
metadata:
2956
name: sample-table-with-sse
3057
spec:

pkg/clients/aws.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,10 @@ func Wrap(err error, msg string) error {
803803
// the underlying error. So, we need to strip off the unique request ID
804804
// manually.
805805
if v1RequestError, ok := err.(awserr.RequestFailure); ok {
806+
// TODO(negz): This loses context about the underlying error
807+
// type, preventing us from using errors.As to figure out what
808+
// kind of error it is. Could we do this without losing
809+
// context?
806810
return errors.Wrap(errors.New(strings.ReplaceAll(err.Error(), v1RequestError.RequestID(), "")), msg)
807811
}
808812
return errors.Wrap(err, msg)

pkg/controller/dynamodb/table/hooks.go

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"encoding/json"
2222
"sort"
23+
"strings"
2324
"time"
2425

2526
svcsdk "github.com/aws/aws-sdk-go/service/dynamodb"
@@ -52,6 +53,7 @@ func SetupTable(mgr ctrl.Manager, l logging.Logger, rl workqueue.RateLimiter, po
5253
e.postObserve = postObserve
5354
e.preCreate = preCreate
5455
e.preDelete = preDelete
56+
e.postDelete = postDelete
5557
e.lateInitialize = lateInitialize
5658
e.isUpToDate = isUpToDate
5759
u := &updateClient{client: e.client}
@@ -89,6 +91,20 @@ func preDelete(_ context.Context, cr *svcapitypes.Table, obj *svcsdk.DeleteTable
8991
return false, nil
9092
}
9193

94+
func postDelete(_ context.Context, _ *svcapitypes.Table, _ *svcsdk.DeleteTableOutput, err error) error {
95+
if err == nil {
96+
return nil
97+
}
98+
// The DynamoDB API returns this error when you try to delete a table
99+
// that is already being deleted. Unfortunately we're passed a v1 SDK
100+
// error that has been munged by aws.Wrap, so we can't use errors.As to
101+
// identify it and must fall back to string matching.
102+
if strings.Contains(err.Error(), "ResourceInUseException") {
103+
return nil
104+
}
105+
return err
106+
}
107+
92108
func postObserve(_ context.Context, cr *svcapitypes.Table, resp *svcsdk.DescribeTableOutput, obs managed.ExternalObservation, err error) (managed.ExternalObservation, error) {
93109
if err != nil {
94110
return managed.ExternalObservation{}, err
@@ -144,8 +160,6 @@ func (e *tagger) Initialize(ctx context.Context, mg resource.Managed) error {
144160
return errors.Wrap(e.kube.Update(ctx, cr), "cannot update Table Spec")
145161
}
146162

147-
// NOTE(muvaf): The rest is taken from manually written controller.
148-
149163
func lateInitialize(in *svcapitypes.TableParameters, t *svcsdk.DescribeTableOutput) error { // nolint:gocyclo,unparam
150164
if t == nil {
151165
return nil
@@ -166,7 +180,20 @@ func lateInitialize(in *svcapitypes.TableParameters, t *svcsdk.DescribeTableOutp
166180
if in.KeySchema == nil && len(t.Table.KeySchema) != 0 {
167181
in.KeySchema = buildAlphaKeyElements(t.Table.KeySchema)
168182
}
169-
183+
if in.BillingMode == nil {
184+
// NOTE(negz): As far as I can tell DescribeTableOutput only
185+
// includes a BillingModeSummary when the billing mode is set to
186+
// PAY_PER_REQUEST. PROVISIONED seems to be he implied default.
187+
// Late initializing a value that the API only implies is
188+
// perhaps a bit of a violation, but it avoids a false positive
189+
// in our IsUpToDate logic which would otherwise detect a diff
190+
// between our desired state (PROVISIONED) and the actual state
191+
// (unspecified).
192+
in.BillingMode = aws.String(svcsdk.BillingModeProvisioned)
193+
if t.Table.BillingModeSummary != nil {
194+
in.BillingMode = t.Table.BillingModeSummary.BillingMode
195+
}
196+
}
170197
if in.ProvisionedThroughput == nil && t.Table.ProvisionedThroughput != nil {
171198
in.ProvisionedThroughput = &svcapitypes.ProvisionedThroughput{
172199
ReadCapacityUnits: t.Table.ProvisionedThroughput.ReadCapacityUnits,
@@ -187,17 +214,20 @@ func lateInitialize(in *svcapitypes.TableParameters, t *svcsdk.DescribeTableOutp
187214
in.SSESpecification.SSEType = t.Table.SSEDescription.SSEType
188215
}
189216
}
190-
if in.StreamSpecification == nil && t.Table.StreamSpecification != nil {
191-
in.StreamSpecification = &svcapitypes.StreamSpecification{
192-
StreamEnabled: t.Table.StreamSpecification.StreamEnabled,
193-
StreamViewType: t.Table.StreamSpecification.StreamViewType,
217+
if in.StreamSpecification == nil {
218+
// NOTE(negz): We late initialize StreamEnabled to false to
219+
// avoid IsUpToDate thinking it needs to explicitly make an
220+
// update to set StreamEnabled to false. DescribeTableOutput
221+
// omits StreamSpecification entirely when it's not enabled.
222+
in.StreamSpecification = &svcapitypes.StreamSpecification{StreamEnabled: aws.Bool(false, aws.FieldRequired)}
223+
if t.Table.StreamSpecification != nil {
224+
in.StreamSpecification = &svcapitypes.StreamSpecification{
225+
StreamEnabled: t.Table.StreamSpecification.StreamEnabled,
226+
StreamViewType: t.Table.StreamSpecification.StreamViewType,
227+
}
194228
}
195229
}
196230

197-
if in.BillingMode == nil && t.Table.BillingModeSummary != nil {
198-
in.BillingMode = t.Table.BillingModeSummary.BillingMode
199-
}
200-
201231
return nil
202232
}
203233

@@ -290,10 +320,11 @@ func createPatch(in *svcsdk.DescribeTableOutput, target *svcapitypes.TableParame
290320
}
291321

292322
func isUpToDate(cr *svcapitypes.Table, resp *svcsdk.DescribeTableOutput) (bool, error) {
293-
// A table that's currently updating or creating can't be updated, so we
294-
// temporarily consider it to be up-to-date no matter what.
323+
// A table that's currently creating, deleting, or updating can't be
324+
// updated, so we temporarily consider it to be up-to-date no matter
325+
// what.
295326
switch aws.StringValue(cr.Status.AtProvider.TableStatus) {
296-
case string(svcapitypes.TableStatus_SDK_UPDATING), string(svcapitypes.TableStatus_SDK_CREATING):
327+
case string(svcapitypes.TableStatus_SDK_UPDATING), string(svcapitypes.TableStatus_SDK_CREATING), string(svcapitypes.TableStatus_SDK_DELETING):
297328
return true, nil
298329
}
299330

@@ -308,14 +339,25 @@ func isUpToDate(cr *svcapitypes.Table, resp *svcsdk.DescribeTableOutput) (bool,
308339
return false, err
309340
}
310341

342+
// TODO(negz): Support updating tags if possible.
343+
// https://github.com/crossplane/provider-aws/issues/945
344+
311345
// At least one of ProvisionedThroughput, BillingMode, UpdateStreamEnabled,
312346
// GlobalSecondaryIndexUpdates or SSESpecification or ReplicaUpdates is
313347
// required.
314348
switch {
315-
case patch.ProvisionedThroughput != nil:
316-
return false, nil
317349
case patch.BillingMode != nil:
318350
return false, nil
351+
case patch.ProvisionedThroughput != nil:
352+
// TODO(negz): DescribeTableOutput appears to report that
353+
// ProvisionedThroughput is 0 when the billing mode is set to
354+
// PAY_PER_REQUEST. This means that if the billing mode is
355+
// changed from PROVISIONED to PAY_PER_REQUEST the provisioned
356+
// throughput must be set to 0 read and write or Crossplane will
357+
// think an update is needed and the update will fail because
358+
// you can't set provisioned throughput when the billing mode is
359+
// set to PAY_PER_REQUEST.
360+
return false, nil
319361
case patch.StreamSpecification != nil:
320362
return false, nil
321363
case len(diffGlobalSecondaryIndexes(GenerateGlobalSecondaryIndexDescriptions(cr.Spec.ForProvider.GlobalSecondaryIndexes), resp.Table.GlobalSecondaryIndexes)) != 0:
@@ -329,7 +371,6 @@ type updateClient struct {
329371
}
330372

331373
func (e *updateClient) preUpdate(ctx context.Context, cr *svcapitypes.Table, u *svcsdk.UpdateTableInput) error {
332-
333374
filtered := &svcsdk.UpdateTableInput{
334375
TableName: aws.String(meta.GetExternalName(cr)),
335376
AttributeDefinitions: u.AttributeDefinitions,
@@ -358,7 +399,17 @@ func (e *updateClient) preUpdate(ctx context.Context, cr *svcapitypes.Table, u *
358399
}
359400
gsiUpdates := diffGlobalSecondaryIndexes(GenerateGlobalSecondaryIndexDescriptions(cr.Spec.ForProvider.GlobalSecondaryIndexes), out.Table.GlobalSecondaryIndexes)
360401
switch {
402+
case p.BillingMode != nil:
403+
filtered.BillingMode = u.BillingMode
404+
405+
// NOTE(negz): You must include provisioned throughput when
406+
// updating the billing mode to PROVISIONED.
407+
if aws.StringValue(u.BillingMode) == string(svcapitypes.BillingMode_PROVISIONED) {
408+
filtered.ProvisionedThroughput = u.ProvisionedThroughput
409+
}
361410
case p.ProvisionedThroughput != nil:
411+
// NOTE(negz): You may only included provisioned throughput when
412+
// the billing mode is PROVISIONED.
362413
filtered.ProvisionedThroughput = u.ProvisionedThroughput
363414
case p.StreamSpecification != nil:
364415
// NOTE(muvaf): Unless StreamEnabled is changed, updating stream
@@ -374,8 +425,6 @@ func (e *updateClient) preUpdate(ctx context.Context, cr *svcapitypes.Table, u *
374425
if p.SSESpecification.KMSMasterKeyID != nil {
375426
filtered.SSESpecification.KMSMasterKeyId = u.SSESpecification.KMSMasterKeyId
376427
}
377-
case p.BillingMode != nil:
378-
filtered.BillingMode = u.BillingMode
379428
case len(gsiUpdates) != 0:
380429
filtered.SetGlobalSecondaryIndexUpdates(gsiUpdates)
381430
}

0 commit comments

Comments
 (0)