Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.

Commit 73bd30e

Browse files
Gabriela Vasselaigvasselai
authored andcommitted
Adding ArchivePolicy to Topic attributes
1 parent 50a71b0 commit 73bd30e

File tree

10 files changed

+180
-1
lines changed

10 files changed

+180
-1
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ pre-commit run --all-files
3232
mvn verify
3333
```
3434

35+
Check the README files in these folders:
36+
```
37+
./aws-sns-subscription
38+
./aws-sns-topic
39+
./aws-sns-topicpolicy
40+
```
41+
3542
Deploying SNS Resources to an AWS Account
3643
-----------------------------------------
3744

aws-sns-topic/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ The [AWS::SNS::Topic](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserG
77
To make changes to the **Topic** resource:
88

99
1. Update the JSON schema `aws-sns-topic.json`
10+
1. Update contract tests
11+
1. Generate sources
1012
1. Implement your changes to the resource handlers
1113

1214
The CloudFormation CLI automatically generates the correct resource model from the schema, whenever the project is built via Maven. You can also do this manually, using the following command: `cfn generate`.

aws-sns-topic/aws-sns-topic.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
"description": "Enables content-based deduplication for FIFO topics. By default, ContentBasedDeduplication is set to false. If you create a FIFO topic and this attribute is false, you must specify a value for the MessageDeduplicationId parameter for the Publish action.\n\nWhen you set ContentBasedDeduplication to true, Amazon SNS uses a SHA-256 hash to generate the MessageDeduplicationId using the body of the message (but not the attributes of the message).\n\n(Optional) To override the generated value, you can specify a value for the the MessageDeduplicationId parameter for the Publish action.\n\n",
3434
"type": "boolean"
3535
},
36+
"ArchivePolicy": {
37+
"description": "The archive policy determines the number of days Amazon SNS retains messages. You can set a retention period from 1 to 365 days.",
38+
"type": "object"
39+
},
3640
"Tags": {
3741
"type": "array",
3842
"uniqueItems": false,

aws-sns-topic/docs/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ To declare this entity in your AWS CloudFormation template, use the following sy
1818
"<a href="#subscription" title="Subscription">Subscription</a>" : <i>[ [ <a href="subscription.md">Subscription</a>, ... ], ... ]</i>,
1919
"<a href="#fifotopic" title="FifoTopic">FifoTopic</a>" : <i>Boolean</i>,
2020
"<a href="#contentbaseddeduplication" title="ContentBasedDeduplication">ContentBasedDeduplication</a>" : <i>Boolean</i>,
21+
"<a href="#archivepolicy" title="ArchivePolicy">ArchivePolicy</a>" : <i>Map</i>,
2122
"<a href="#tags" title="Tags">Tags</a>" : <i>[ <a href="tag.md">Tag</a>, ... ]</i>,
2223
"<a href="#topicname" title="TopicName">TopicName</a>" : <i>String</i>,
2324
"<a href="#signatureversion" title="SignatureVersion">SignatureVersion</a>" : <i>String</i>,
@@ -39,6 +40,7 @@ Properties:
3940
- <a href="subscription.md">Subscription</a></i>
4041
<a href="#fifotopic" title="FifoTopic">FifoTopic</a>: <i>Boolean</i>
4142
<a href="#contentbaseddeduplication" title="ContentBasedDeduplication">ContentBasedDeduplication</a>: <i>Boolean</i>
43+
<a href="#archivepolicy" title="ArchivePolicy">ArchivePolicy</a>: <i>Map</i>
4244
<a href="#tags" title="Tags">Tags</a>: <i>
4345
- <a href="tag.md">Tag</a></i>
4446
<a href="#topicname" title="TopicName">TopicName</a>: <i>String</i>
@@ -122,6 +124,16 @@ _Type_: Boolean
122124

123125
_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
124126

127+
#### ArchivePolicy
128+
129+
The archive policy determines the number of days Amazon SNS retains messages. You can set a retention period from 1 to 365 days.
130+
131+
_Required_: No
132+
133+
_Type_: Map
134+
135+
_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt)
136+
125137
#### Tags
126138

127139
_Required_: No

aws-sns-topic/src/main/java/software/amazon/sns/topic/TopicAttributeName.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ public enum TopicAttributeName {
77
CONTENT_BASED_DEDUPLICATION("ContentBasedDeduplication"),
88
FIFO_TOPIC("FifoTopic"),
99
SIGNATURE_VERSION("SignatureVersion"),
10-
TRACING_CONFIG("TracingConfig");
10+
TRACING_CONFIG("TracingConfig"),
11+
ARCHIVE_POLICY("ArchivePolicy"),
12+
;
1113

1214
private final String value;
1315

aws-sns-topic/src/main/java/software/amazon/sns/topic/Translator.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
*/
3939

4040
public class Translator {
41+
public static final String EMPTY_POLICY = "{}";
4142
private static final ObjectMapper MAPPER = new ObjectMapper();
4243

4344
static CreateTopicRequest translateToCreateTopicRequest(final ResourceModel model, Map<String, String> desiredResourceTags) {
@@ -137,6 +138,7 @@ static ResourceModel translateFromGetTopicAttributes(GetTopicAttributesResponse
137138
.displayName(nullIfEmpty(attributes.get(TopicAttributeName.DISPLAY_NAME.toString())))
138139
.kmsMasterKeyId(nullIfEmpty(attributes.get(TopicAttributeName.KMS_MASTER_KEY_ID.toString())))
139140
.signatureVersion(nullIfEmpty(attributes.get(TopicAttributeName.SIGNATURE_VERSION.toString())))
141+
.archivePolicy(convertArchivePolicyToMap(attributes.get(TopicAttributeName.ARCHIVE_POLICY.toString())))
140142
.tracingConfig(nullIfEmpty(attributes.get(TopicAttributeName.TRACING_CONFIG.toString())))
141143
.fifoTopic(nullIfEmptyBoolean(attributes.get(TopicAttributeName.FIFO_TOPIC.toString())))
142144
.contentBasedDeduplication(nullIfEmptyBoolean(attributes.get(TopicAttributeName.CONTENT_BASED_DEDUPLICATION.toString())))
@@ -238,6 +240,53 @@ static String getDataProtectionPolicyAsString(final ResourceModel model) {
238240
}
239241
}
240242

243+
/**
244+
* null -> null
245+
* Empty Map -> "{}"
246+
*/
247+
public static String getArchivePolicyAsString(ResourceModel model) {
248+
return getArchivePolicyAsString(model, null);
249+
}
250+
251+
/**
252+
* null -> defaultOnNull
253+
* Empty Map -> "{}"
254+
*/
255+
public static String getArchivePolicyAsString(ResourceModel model, String defaultOnNull) {
256+
if (model.getArchivePolicy() == null) {
257+
return defaultOnNull;
258+
}
259+
try {
260+
return MAPPER.writeValueAsString(model.getArchivePolicy());
261+
} catch (JsonProcessingException e) {
262+
throw new CfnInvalidRequestException(e);
263+
}
264+
}
265+
266+
/**
267+
* null -> Empty Map
268+
* "" -> Empty Map
269+
* " " -> Empty Map
270+
* "{}" -> Empty Map
271+
*/
272+
private static Map<String,Object> convertArchivePolicyToMap(String jsonString) {
273+
if (StringUtils.isBlank(jsonString)) {
274+
return Collections.emptyMap();
275+
}
276+
277+
try {
278+
return MAPPER.readValue(jsonString, new TypeReference<Map<String, Object>>() {});
279+
} catch (Exception e) {
280+
throw new CfnInvalidRequestException(e);
281+
}
282+
}
283+
284+
/**
285+
* null -> null
286+
* "" -> null
287+
* " " -> null
288+
* "{}" -> Empty Map
289+
*/
241290
private static Map<String,Object> convertToJson(String jsonString) {
242291
Map<String, Object> obj = null;
243292

@@ -264,6 +313,7 @@ private static Map<String, String> translateTopicAttributesToMap(ResourceModel m
264313
putIfNotNull(attributes, TopicAttributeName.KMS_MASTER_KEY_ID.toString(), model.getKmsMasterKeyId());
265314
putIfNotNull(attributes, TopicAttributeName.SIGNATURE_VERSION.toString(), model.getSignatureVersion());
266315
putIfNotNull(attributes, TopicAttributeName.TRACING_CONFIG.toString(), model.getTracingConfig());
316+
putIfNotNull(attributes, TopicAttributeName.ARCHIVE_POLICY.toString(), getArchivePolicyAsString(model));
267317
putIfNotNull(attributes, TopicAttributeName.FIFO_TOPIC.toString(), model.getFifoTopic());
268318
putIfNotNull(attributes, TopicAttributeName.CONTENT_BASED_DEDUPLICATION.toString(), model.getContentBasedDeduplication());
269319
return attributes;

aws-sns-topic/src/main/java/software/amazon/sns/topic/UpdateHandler.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import java.util.ArrayList;
2727
import java.util.stream.Collectors;
2828

29+
import static software.amazon.sns.topic.Translator.EMPTY_POLICY;
30+
2931
public class UpdateHandler extends BaseHandlerStd {
3032
private Logger logger;
3133

@@ -103,6 +105,17 @@ protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
103105
}
104106
return progress;
105107
})
108+
.then(progress -> {
109+
String previousArchivePolicy = Translator.getArchivePolicyAsString(previousModel, EMPTY_POLICY);
110+
String desiredArchivePolicy = Translator.getArchivePolicyAsString(model, EMPTY_POLICY);
111+
if(!StringUtils.equals(previousArchivePolicy, desiredArchivePolicy)) {
112+
return proxy.initiate("AWS-SNS-Topic::Update::ArchivePolicy", proxyClient, model, callbackContext)
113+
.translateToServiceRequest(m -> Translator.translateToSetAttributesRequest(m.getTopicArn(), TopicAttributeName.ARCHIVE_POLICY, desiredArchivePolicy))
114+
.makeServiceCall((setTopicAttributesRequest, client) -> proxy.injectCredentialsAndInvokeV2(setTopicAttributesRequest, client.client()::setTopicAttributes))
115+
.progress();
116+
}
117+
return progress;
118+
})
106119
.then(progress -> {
107120
String previousVal = previousModel.getContentBasedDeduplication() != null ? previousModel.getContentBasedDeduplication().toString() : "false";
108121
String desiredVal = model.getContentBasedDeduplication() != null ? model.getContentBasedDeduplication().toString() : "false";

aws-sns-topic/src/test/java/software/amazon/sns/topic/CreateHandlerTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package software.amazon.sns.topic;
22

3+
import com.google.common.collect.ImmutableMap;
34
import org.assertj.core.util.Maps;
45
import org.junit.jupiter.api.AfterEach;
56
import org.junit.jupiter.api.BeforeEach;
@@ -149,13 +150,15 @@ public void handleRequest_SimpleSuccess_WithAttributes() {
149150
.displayName("sns-topic")
150151
.kmsMasterKeyId("dummy-kms-key-id")
151152
.signatureVersion("2")
153+
.archivePolicy(ImmutableMap.of("MessageRetentionPeriod", "90"))
152154
.tracingConfig(TracingMode.ACTIVE.toString())
153155
.build();
154156

155157
Map<String, String> attributes = new HashMap<>();
156158
attributes.put(TopicAttributeName.TOPIC_ARN.toString(), "arn:aws:sns:us-east-1:123456789012:sns-topic-name");
157159
attributes.put(TopicAttributeName.SIGNATURE_VERSION.toString(), "2");
158160
attributes.put(TopicAttributeName.TRACING_CONFIG.toString(), TracingMode.ACTIVE.toString());
161+
attributes.put(TopicAttributeName.ARCHIVE_POLICY.toString(), "\"MessageRetentionPeriod\":\"90\"");
159162
final GetTopicAttributesResponse getTopicAttributesResponse = GetTopicAttributesResponse.builder()
160163
.attributes(attributes)
161164
.build();

aws-sns-topic/src/test/java/software/amazon/sns/topic/ReadHandlerTest.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.Map;
77
import java.util.List;
88

9+
import com.google.common.collect.ImmutableMap;
910
import software.amazon.awssdk.services.sns.SnsClient;
1011
import software.amazon.awssdk.services.sns.model.*;
1112
import software.amazon.cloudformation.exceptions.CfnGeneralServiceException;
@@ -76,6 +77,7 @@ public void handleRequest_SimpleSuccess() {
7677
.tracingConfig(TracingMode.PASS_THROUGH.toString())
7778
.tags(tags)
7879
.dataProtectionPolicy(policy)
80+
.archivePolicy(ImmutableMap.of("MessageRetentionPeriod", "30"))
7981
.build();
8082

8183

@@ -84,6 +86,7 @@ public void handleRequest_SimpleSuccess() {
8486
attributes.put(TopicAttributeName.TOPIC_ARN.toString(), "arn:aws:sns:us-east-1:123456789012:sns-topic-name");
8587
attributes.put(TopicAttributeName.SIGNATURE_VERSION.toString(), "2");
8688
attributes.put(TopicAttributeName.TRACING_CONFIG.toString(), TracingMode.PASS_THROUGH.toString());
89+
attributes.put(TopicAttributeName.ARCHIVE_POLICY.toString(), "{\"MessageRetentionPeriod\":\"30\"}");
8790

8891
final GetTopicAttributesResponse getTopicAttributesResponse = GetTopicAttributesResponse.builder()
8992
.attributes(attributes)
@@ -133,6 +136,7 @@ public void handleRequest_attributesForDriftDetection() {
133136
final String topicDisplayName = "topic-display-name";
134137
final String signatureVersion = "2";
135138
final String tracingConfig = TracingMode.ACTIVE.toString();
139+
final Map<String, Object> archivePolicy = ImmutableMap.of("MessageRetentionPeriod", "60");
136140

137141
final ResourceModel model = ResourceModel.builder()
138142
.topicArn(topicArn)
@@ -141,6 +145,7 @@ public void handleRequest_attributesForDriftDetection() {
141145
.subscription(subscriptions)
142146
.signatureVersion(signatureVersion)
143147
.tracingConfig(tracingConfig)
148+
.archivePolicy(archivePolicy)
144149
.tags(tags)
145150
.build();
146151

@@ -151,6 +156,7 @@ public void handleRequest_attributesForDriftDetection() {
151156
attributes.put(TopicAttributeName.SIGNATURE_VERSION.toString(), signatureVersion);
152157
attributes.put(TopicAttributeName.TRACING_CONFIG.toString(), tracingConfig);
153158
attributes.put(TopicAttributeName.CONTENT_BASED_DEDUPLICATION.toString(), "true");
159+
attributes.put(TopicAttributeName.ARCHIVE_POLICY.toString(), "{\"MessageRetentionPeriod\":\"60\"}");
154160
final GetTopicAttributesResponse getTopicAttributesResponse = GetTopicAttributesResponse.builder()
155161
.attributes(attributes)
156162
.build();
@@ -363,4 +369,55 @@ public void handleRequest_GetDataProtectionPolicyThrottleException() {
363369
final ResourceHandlerRequest<ResourceModel> request = ResourceHandlerRequest.<ResourceModel>builder().desiredResourceState(model).build();
364370
assertThrows(CfnThrottlingException.class, () -> handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger));
365371
}
372+
373+
@Test
374+
public void handleRequest_driftArchivePolicyWhenSetEmpty() {
375+
final List<Subscription> subscriptions = new ArrayList<>();
376+
final List<Tag> tags = new ArrayList<>();
377+
tags.add(Tag.builder().key("key1").value("value1").build());
378+
379+
final String topicArn = "arn:aws:sns:us-east-1:123456789012:sns-topic-name.fifo";
380+
final String topicName = "sns-topic-name.fifo";
381+
final String topicDisplayName = "topic-display-name";
382+
final String signatureVersion = "2";
383+
final String tracingConfig = TracingMode.ACTIVE.toString();
384+
final Map<String, Object> archivePolicy = new HashMap<>();
385+
386+
final ResourceModel model = ResourceModel.builder()
387+
.topicArn(topicArn)
388+
.topicName(topicName)
389+
.displayName(topicDisplayName)
390+
.subscription(subscriptions)
391+
.signatureVersion(signatureVersion)
392+
.tracingConfig(tracingConfig)
393+
.archivePolicy(archivePolicy)
394+
.tags(tags)
395+
.build();
396+
397+
Map<String, String> attributes = new HashMap<>();
398+
attributes.put(TopicAttributeName.DISPLAY_NAME.toString(), topicDisplayName);
399+
attributes.put(TopicAttributeName.TOPIC_ARN.toString(), topicArn);
400+
attributes.put(TopicAttributeName.FIFO_TOPIC.toString(), "true");
401+
attributes.put(TopicAttributeName.SIGNATURE_VERSION.toString(), signatureVersion);
402+
attributes.put(TopicAttributeName.TRACING_CONFIG.toString(), tracingConfig);
403+
attributes.put(TopicAttributeName.CONTENT_BASED_DEDUPLICATION.toString(), "true");
404+
final GetTopicAttributesResponse getTopicAttributesResponse = GetTopicAttributesResponse.builder()
405+
.attributes(attributes)
406+
.build();
407+
when(proxyClient.client().getTopicAttributes(any(GetTopicAttributesRequest.class))).thenReturn(getTopicAttributesResponse);
408+
when(proxyClient.client().listSubscriptionsByTopic(any(ListSubscriptionsByTopicRequest.class))).thenReturn(ListSubscriptionsByTopicResponse.builder().build());
409+
when(proxyClient.client().listTagsForResource(any(ListTagsForResourceRequest.class))).thenReturn(ListTagsForResourceResponse.builder().build());
410+
411+
final ResourceHandlerRequest<ResourceModel> request = ResourceHandlerRequest.<ResourceModel>builder().desiredResourceState(model).build();
412+
final ProgressEvent<ResourceModel, CallbackContext> response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger);
413+
414+
assertThat(response).isNotNull();
415+
assertThat(response.getResourceModel().getArchivePolicy()).isNotNull();
416+
assertThat(response.getResourceModel().getArchivePolicy()).isEmpty();
417+
418+
verify(proxyClient.client()).getTopicAttributes(any(GetTopicAttributesRequest.class));
419+
verify(proxyClient.client()).listSubscriptionsByTopic(any(ListSubscriptionsByTopicRequest.class));
420+
verify(proxyClient.client()).listTagsForResource(any(ListTagsForResourceRequest.class));
421+
verify(proxyClient.client(), never()).getDataProtectionPolicy(any(GetDataProtectionPolicyRequest.class));
422+
}
366423
}

aws-sns-topic/src/test/java/software/amazon/sns/topic/UpdateHandlerTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package software.amazon.sns.topic;
22

3+
import com.google.common.collect.ImmutableMap;
34
import com.google.common.collect.Maps;
45
import org.junit.jupiter.api.AfterEach;
56
import org.junit.jupiter.api.BeforeEach;
@@ -159,6 +160,34 @@ public void handleRequest_SimpleSuccess_UpdateTracingConfig() {
159160
verify(proxyClient.client()).getDataProtectionPolicy(any(GetDataProtectionPolicyRequest.class));
160161
}
161162

163+
@Test
164+
public void handleRequest_SimpleSuccess_UpdateArchivePolicy() {
165+
final ResourceModel model = ResourceModel.builder()
166+
.topicArn("arn:aws:sns:us-east-1:123456789012:sns-topic-name.fifo")
167+
.archivePolicy(ImmutableMap.of("MessageRetentionPeriod", "1"))
168+
.build();
169+
170+
final ResourceModel previousModel = ResourceModel.builder()
171+
.topicArn("arn:aws:sns:us-east-1:123456789012:sns-topic-name.fifo")
172+
.archivePolicy(ImmutableMap.of("MessageRetentionPeriod", "365"))
173+
.build();
174+
175+
Map<String, String> attributes = new HashMap<>();
176+
attributes.put(TopicAttributeName.ARCHIVE_POLICY.toString(), "{\"MessageRetentionPeriod\":\"365\"}");
177+
attributes.put(TopicAttributeName.TOPIC_ARN.toString(), "arn:aws:sns:us-east-1:123456789012:sns-topic-name.fifo");
178+
setupUpdateHandlerMocks(attributes);
179+
180+
final ResourceHandlerRequest<ResourceModel> request = ResourceHandlerRequest.<ResourceModel>builder().desiredResourceState(model).previousResourceState(previousModel).build();
181+
final ProgressEvent<ResourceModel, CallbackContext> response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger);
182+
183+
validateResponseSuccess(response);
184+
verify(proxyClient.client()).setTopicAttributes(any(SetTopicAttributesRequest.class));
185+
verify(proxyClient.client(), times(2)).getTopicAttributes(any(GetTopicAttributesRequest.class));
186+
verify(proxyClient.client(), times(1)).setTopicAttributes(any(SetTopicAttributesRequest.class));
187+
verify(proxyClient.client(), times(2)).listSubscriptionsByTopic(any(ListSubscriptionsByTopicRequest.class));
188+
verify(proxyClient.client()).getDataProtectionPolicy(any(GetDataProtectionPolicyRequest.class));
189+
}
190+
162191
@Test
163192
public void handleRequest_SimpleSuccess_RemoveActiveTracingConfig() {
164193
final ResourceModel model = ResourceModel.builder()

0 commit comments

Comments
 (0)