Skip to content

Commit f868396

Browse files
author
skagalwala
committed
[feat]: allow adding resource policy to dynamodb tables
1 parent 156aeb3 commit f868396

File tree

11 files changed

+299
-39
lines changed

11 files changed

+299
-39
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
ack_generate_info:
2-
build_date: "2025-09-19T17:14:03Z"
2+
build_date: "2025-10-17T18:47:29Z"
33
build_hash: 6b4211163dcc34776b01da9a18217bac0f4103fd
44
go_version: go1.24.6
55
version: v0.52.0
6-
api_directory_checksum: bcdceff2d7ddf7c98141572260ef2e6cee8bf23f
6+
api_directory_checksum: d2887bf57c4e94a2687e17c41f74c875131c0beb
77
api_version: v1alpha1
88
aws_sdk_go_version: v1.32.6
99
generator_config_info:
10-
file_checksum: cc3489b53a45170d339a4de0d7d7ec0aa788955e
10+
file_checksum: 3c4832feff83bc9c29b40bc73bafc1d7e75ab1cd
1111
original_file_name: generator.yaml
1212
last_modification:
1313
reason: API generation

apis/v1alpha1/generator.yaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ ignore:
1111
# Replica of Spec.SSESpecification
1212
- TableDescription.SSEDescription
1313
- TableDescription.TableClassSummary
14-
- CreateTableInput.ResourcePolicy
1514
- CreateTableInput.WarmThroughput
1615
operations:
1716
UpdateGlobalTable:
@@ -29,7 +28,13 @@ resources:
2928
custom_field:
3029
list_of: CreateReplicationGroupMemberAction
3130
compare:
32-
is_ignored: true
31+
is_ignored: true
32+
ResourcePolicy:
33+
from:
34+
operation: PutResourcePolicy
35+
path: Policy
36+
compare:
37+
is_ignored: true
3338
GlobalSecondaryIndexesDescriptions:
3439
custom_field:
3540
list_of: GlobalSecondaryIndexDescription
@@ -74,7 +79,7 @@ resources:
7479
from:
7580
operation: UpdateContributorInsights
7681
path: ContributorInsightsAction
77-
compare:
82+
compare:
7883
is_ignored: true
7984
exceptions:
8085
errors:

apis/v1alpha1/table.go

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apis/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/dynamodb.services.k8s.aws_tables.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,21 @@ spec:
325325
format: int64
326326
type: integer
327327
type: object
328+
resourcePolicy:
329+
description: |-
330+
An Amazon Web Services resource-based policy document in JSON format.
331+
332+
* The maximum size supported for a resource-based policy document is 20
333+
KB. DynamoDB counts whitespaces when calculating the size of a policy
334+
against this limit.
335+
336+
* Within a resource-based policy, if the action for a DynamoDB service-linked
337+
role (SLR) to replicate data for a global table is denied, adding or deleting
338+
a replica will fail with an error.
339+
340+
For a full list of all considerations that apply while attaching a resource-based
341+
policy, see Resource-based policy considerations (https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/rbac-considerations.html).
342+
type: string
328343
sseSpecification:
329344
description: Represents the settings used to enable server-side encryption.
330345
properties:

generator.yaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ ignore:
1111
# Replica of Spec.SSESpecification
1212
- TableDescription.SSEDescription
1313
- TableDescription.TableClassSummary
14-
- CreateTableInput.ResourcePolicy
1514
- CreateTableInput.WarmThroughput
1615
operations:
1716
UpdateGlobalTable:
@@ -29,7 +28,13 @@ resources:
2928
custom_field:
3029
list_of: CreateReplicationGroupMemberAction
3130
compare:
32-
is_ignored: true
31+
is_ignored: true
32+
ResourcePolicy:
33+
from:
34+
operation: PutResourcePolicy
35+
path: Policy
36+
compare:
37+
is_ignored: true
3338
GlobalSecondaryIndexesDescriptions:
3439
custom_field:
3540
list_of: GlobalSecondaryIndexDescription
@@ -74,7 +79,7 @@ resources:
7479
from:
7580
operation: UpdateContributorInsights
7681
path: ContributorInsightsAction
77-
compare:
82+
compare:
7883
is_ignored: true
7984
exceptions:
8085
errors:

helm/crds/dynamodb.services.k8s.aws_tables.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,21 @@ spec:
329329
format: int64
330330
type: integer
331331
type: object
332+
resourcePolicy:
333+
description: |-
334+
An Amazon Web Services resource-based policy document in JSON format.
335+
336+
- The maximum size supported for a resource-based policy document is 20
337+
KB. DynamoDB counts whitespaces when calculating the size of a policy
338+
against this limit.
339+
340+
- Within a resource-based policy, if the action for a DynamoDB service-linked
341+
role (SLR) to replicate data for a global table is denied, adding or deleting
342+
a replica will fail with an error.
343+
344+
For a full list of all considerations that apply while attaching a resource-based
345+
policy, see Resource-based policy considerations (https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/rbac-considerations.html).
346+
type: string
332347
sseSpecification:
333348
description: Represents the settings used to enable server-side encryption.
334349
properties:

pkg/resource/table/hooks.go

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,6 @@ func (rm *resourceManager) customUpdateTable(
166166
setSyncedCondition(desired, corev1.ConditionFalse, &msg, nil)
167167
return desired, requeueWaitWhileCreating
168168
}
169-
if isTableUpdating(latest) {
170-
msg := "table is currently being updated"
171-
setSyncedCondition(desired, corev1.ConditionFalse, &msg, nil)
172-
return desired, requeueWaitWhileUpdating
173-
}
174-
if tableHasTerminalStatus(latest) {
175-
msg := "table is in '" + *latest.ko.Status.TableStatus + "' status"
176-
setTerminalCondition(desired, corev1.ConditionTrue, &msg, nil)
177-
setSyncedCondition(desired, corev1.ConditionTrue, nil, nil)
178-
return desired, nil
179-
}
180169

181170
// Merge in the information we read from the API call above to the copy of
182171
// the original Kubernetes object we passed to the function
@@ -188,10 +177,36 @@ func (rm *resourceManager) customUpdateTable(
188177
return nil, err
189178
}
190179
}
191-
if !delta.DifferentExcept("Spec.Tags") {
180+
181+
// ResourcePolicy can be updated independently of table state
182+
if delta.DifferentAt("Spec.ResourcePolicy") {
183+
if latest.ko.Status.ACKResourceMetadata == nil || latest.ko.Status.ACKResourceMetadata.ARN == nil {
184+
rlog.Debug("skipping ResourcePolicy sync - table ARN not available yet")
185+
return &resource{ko}, requeueWaitWhileCreating
186+
}
187+
188+
err = rm.syncResourcePolicy(ctx, desired, latest)
189+
if err != nil {
190+
return nil, fmt.Errorf("cannot update table resource policy %v", err)
191+
}
192+
}
193+
194+
if !delta.DifferentExcept("Spec.Tags", "Spec.ResourcePolicy") {
192195
return &resource{ko}, nil
193196
}
194197

198+
if isTableUpdating(latest) {
199+
msg := "table is currently being updated"
200+
setSyncedCondition(desired, corev1.ConditionFalse, &msg, nil)
201+
return desired, requeueWaitWhileUpdating
202+
}
203+
if tableHasTerminalStatus(latest) {
204+
msg := "table is in '" + *latest.ko.Status.TableStatus + "' status"
205+
setTerminalCondition(desired, corev1.ConditionTrue, &msg, nil)
206+
setSyncedCondition(desired, corev1.ConditionTrue, nil, nil)
207+
return desired, nil
208+
}
209+
195210
if delta.DifferentAt("Spec.TimeToLive") {
196211
if err := rm.syncTTL(ctx, desired, latest); err != nil {
197212
// Ignore "already disabled errors"
@@ -522,6 +537,15 @@ func (rm *resourceManager) setResourceAdditionalFields(
522537
} else {
523538
ko.Spec.ContinuousBackups = pitrSpec
524539
}
540+
541+
if ko.Status.ACKResourceMetadata != nil && ko.Status.ACKResourceMetadata.ARN != nil {
542+
policy, err := rm.getResourcePolicyWithContext(ctx, (*string)(ko.Status.ACKResourceMetadata.ARN))
543+
if err != nil {
544+
return err
545+
}
546+
ko.Spec.ResourcePolicy = policy
547+
}
548+
525549
return nil
526550
}
527551

@@ -671,6 +695,14 @@ func customPreCompare(
671695
}
672696
}
673697

698+
if ackcompare.HasNilDifference(a.ko.Spec.ResourcePolicy, b.ko.Spec.ResourcePolicy) {
699+
delta.Add("Spec.ResourcePolicy", a.ko.Spec.ResourcePolicy, b.ko.Spec.ResourcePolicy)
700+
} else if a.ko.Spec.ResourcePolicy != nil && b.ko.Spec.ResourcePolicy != nil {
701+
if *a.ko.Spec.ResourcePolicy != *b.ko.Spec.ResourcePolicy {
702+
delta.Add("Spec.ResourcePolicy", a.ko.Spec.ResourcePolicy, b.ko.Spec.ResourcePolicy)
703+
}
704+
}
705+
674706
}
675707

676708
// equalAttributeDefinitions return whether two AttributeDefinition arrays are equal or not.
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package table
15+
16+
import (
17+
"context"
18+
"errors"
19+
20+
ackerr "github.com/aws-controllers-k8s/runtime/pkg/errors"
21+
ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log"
22+
svcsdk "github.com/aws/aws-sdk-go-v2/service/dynamodb"
23+
svcsdktypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
24+
)
25+
26+
// syncResourcePolicy updates a DynamoDB table's resource-based policy.
27+
// If the desired policy is nil, it deletes the policy. Otherwise, it puts/updates the policy.
28+
func (rm *resourceManager) syncResourcePolicy(
29+
ctx context.Context,
30+
desired *resource,
31+
latest *resource,
32+
) (err error) {
33+
rlog := ackrtlog.FromContext(ctx)
34+
exit := rlog.Trace("rm.syncResourcePolicy")
35+
defer func(err error) { exit(err) }(err)
36+
37+
if desired.ko.Spec.ResourcePolicy == nil {
38+
return rm.deleteResourcePolicy(ctx, latest)
39+
}
40+
41+
return rm.putResourcePolicy(ctx, desired)
42+
}
43+
44+
// putResourcePolicy attaches or updates a resource-based policy to a DynamoDB table.
45+
func (rm *resourceManager) putResourcePolicy(
46+
ctx context.Context,
47+
r *resource,
48+
) (err error) {
49+
rlog := ackrtlog.FromContext(ctx)
50+
exit := rlog.Trace("rm.putResourcePolicy")
51+
defer func(err error) { exit(err) }(err)
52+
53+
if r.ko.Spec.ResourcePolicy == nil {
54+
return nil
55+
}
56+
57+
tableARN := (*string)(r.ko.Status.ACKResourceMetadata.ARN)
58+
if tableARN == nil || *tableARN == "" {
59+
return errors.New("table ARN is required to put resource policy")
60+
}
61+
62+
_, err = rm.sdkapi.PutResourcePolicy(
63+
ctx,
64+
&svcsdk.PutResourcePolicyInput{
65+
ResourceArn: tableARN,
66+
Policy: r.ko.Spec.ResourcePolicy,
67+
},
68+
)
69+
rm.metrics.RecordAPICall("UPDATE", "PutResourcePolicy", err)
70+
return err
71+
}
72+
73+
// deleteResourcePolicy removes a resource-based policy from a DynamoDB table.
74+
func (rm *resourceManager) deleteResourcePolicy(
75+
ctx context.Context,
76+
r *resource,
77+
) (err error) {
78+
rlog := ackrtlog.FromContext(ctx)
79+
exit := rlog.Trace("rm.deleteResourcePolicy")
80+
defer func(err error) { exit(err) }(err)
81+
82+
tableARN := (*string)(r.ko.Status.ACKResourceMetadata.ARN)
83+
if tableARN == nil || *tableARN == "" {
84+
return errors.New("table ARN is required to delete resource policy")
85+
}
86+
87+
_, err = rm.sdkapi.DeleteResourcePolicy(
88+
ctx,
89+
&svcsdk.DeleteResourcePolicyInput{
90+
ResourceArn: tableARN,
91+
},
92+
)
93+
94+
// DeleteResourcePolicy is idempotent - PolicyNotFoundException means already deleted
95+
if err != nil {
96+
var policyNotFoundErr *svcsdktypes.PolicyNotFoundException
97+
if errors.As(err, &policyNotFoundErr) {
98+
// Policy already doesn't exist, this is a success case
99+
return nil
100+
}
101+
}
102+
103+
rm.metrics.RecordAPICall("DELETE", "DeleteResourcePolicy", err)
104+
return err
105+
}
106+
107+
// getResourcePolicyWithContext retrieves the resource-based policy of a DynamoDB table.
108+
// Returns nil policy (not an error) if no policy exists.
109+
func (rm *resourceManager) getResourcePolicyWithContext(
110+
ctx context.Context,
111+
tableARN *string,
112+
) (*string, error) {
113+
var err error
114+
rlog := ackrtlog.FromContext(ctx)
115+
exit := rlog.Trace("rm.getResourcePolicyWithContext")
116+
defer func(err error) { exit(err) }(err)
117+
118+
if tableARN == nil || *tableARN == "" {
119+
return nil, errors.New("table ARN is required to get resource policy")
120+
}
121+
122+
res, err := rm.sdkapi.GetResourcePolicy(
123+
ctx,
124+
&svcsdk.GetResourcePolicyInput{
125+
ResourceArn: tableARN,
126+
},
127+
)
128+
129+
// If the policy doesn't exist, return nil policy without error (similar to S3's NoSuchBucketPolicy)
130+
if err != nil {
131+
if awsErr, ok := ackerr.AWSError(err); ok && awsErr.ErrorCode() == "PolicyNotFoundException" {
132+
return nil, nil
133+
}
134+
rm.metrics.RecordAPICall("GET", "GetResourcePolicy", err)
135+
return nil, err
136+
}
137+
138+
rm.metrics.RecordAPICall("GET", "GetResourcePolicy", nil)
139+
return res.Policy, nil
140+
}

0 commit comments

Comments
 (0)