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

Commit e62cfd9

Browse files
authored
Update Tags Support for Namespace (#62)
1 parent 7e9d835 commit e62cfd9

File tree

12 files changed

+261
-33
lines changed

12 files changed

+261
-33
lines changed

aws-redshiftserverless-namespace/aws-redshiftserverless-namespace.json

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,16 @@
225225
}
226226
},
227227
"tagging": {
228-
"taggable": false
228+
"taggable": true,
229+
"tagOnCreate": true,
230+
"tagUpdatable": true,
231+
"cloudFormationSystemTags": false,
232+
"tagProperty": "/properties/Tags",
233+
"permissions": [
234+
"redshift-serverless:ListTagsForResource",
235+
"redshift-serverless:TagResource",
236+
"redshift-serverless:UntagResource"
237+
]
229238
},
230239
"required": [
231240
"NamespaceName"
@@ -248,9 +257,6 @@
248257
"/properties/AdminUserPassword",
249258
"/properties/FinalSnapshotName",
250259
"/properties/FinalSnapshotRetentionPeriod",
251-
"/properties/Tags",
252-
"/properties/Tags/*/Key",
253-
"/properties/Tags/*/Value",
254260
"/properties/ManageAdminPassword",
255261
"/properties/RedshiftIdcApplicationArn"
256262
],
@@ -264,6 +270,7 @@
264270
"handlers": {
265271
"create": {
266272
"permissions": [
273+
"iam:CreateServiceLinkedRole",
267274
"iam:PassRole",
268275
"kms:TagResource",
269276
"kms:UntagResource",
@@ -282,6 +289,8 @@
282289
"redshift-serverless:GetNamespace",
283290
"redshift-serverless:ListSnapshotCopyConfigurations",
284291
"redshift-serverless:CreateSnapshotCopyConfiguration",
292+
"redshift-serverless:ListTagsForResource",
293+
"redshift-serverless:TagResource",
285294
"redshift:GetResourcePolicy",
286295
"redshift:PutResourcePolicy",
287296
"secretsmanager:CreateSecret",
@@ -294,6 +303,7 @@
294303
"permissions": [
295304
"iam:PassRole",
296305
"redshift-serverless:GetNamespace",
306+
"redshift-serverless:ListTagsForResource",
297307
"redshift:GetResourcePolicy",
298308
"redshift-serverless:ListSnapshotCopyConfigurations"
299309
]
@@ -320,6 +330,9 @@
320330
"redshift-serverless:CreateSnapshotCopyConfiguration",
321331
"redshift-serverless:UpdateSnapshotCopyConfiguration",
322332
"redshift-serverless:DeleteSnapshotCopyConfiguration",
333+
"redshift-serverless:ListTagsForResource",
334+
"redshift-serverless:TagResource",
335+
"redshift-serverless:UntagResource",
323336
"redshift:GetResourcePolicy",
324337
"redshift:PutResourcePolicy",
325338
"redshift:DeleteResourcePolicy",
@@ -336,6 +349,8 @@
336349
"iam:PassRole",
337350
"redshift-serverless:DeleteNamespace",
338351
"redshift-serverless:GetNamespace",
352+
"redshift-serverless:ListTagsForResource",
353+
"redshift-serverless:UntagResource",
339354
"kms:RetireGrant",
340355
"secretsmanager:DescribeSecret",
341356
"secretsmanager:DeleteSecret",
@@ -345,7 +360,8 @@
345360
"list": {
346361
"permissions": [
347362
"iam:PassRole",
348-
"redshift-serverless:ListNamespaces"
363+
"redshift-serverless:ListNamespaces",
364+
"redshift-serverless:ListTagsForResource"
349365
]
350366
}
351367
},

aws-redshiftserverless-namespace/resource-role.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Resources:
3030
Statement:
3131
- Effect: Allow
3232
Action:
33+
- "iam:CreateServiceLinkedRole"
3334
- "iam:PassRole"
3435
- "kms:CancelKeyDeletion"
3536
- "kms:CreateGrant"
@@ -51,6 +52,9 @@ Resources:
5152
- "redshift-serverless:GetNamespace"
5253
- "redshift-serverless:ListNamespaces"
5354
- "redshift-serverless:ListSnapshotCopyConfigurations"
55+
- "redshift-serverless:ListTagsForResource"
56+
- "redshift-serverless:TagResource"
57+
- "redshift-serverless:UntagResource"
5458
- "redshift-serverless:UpdateNamespace"
5559
- "redshift-serverless:UpdateSnapshotCopyConfiguration"
5660
- "redshift:DeleteResourcePolicy"

aws-redshiftserverless-namespace/src/main/java/software/amazon/redshiftserverless/namespace/BaseHandlerStd.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
import software.amazon.awssdk.services.redshiftserverless.model.InternalServerException;
1111
import software.amazon.awssdk.services.redshiftserverless.model.ListSnapshotCopyConfigurationsRequest;
1212
import software.amazon.awssdk.services.redshiftserverless.model.ListSnapshotCopyConfigurationsResponse;
13+
import software.amazon.awssdk.services.redshiftserverless.model.ListTagsForResourceRequest;
14+
import software.amazon.awssdk.services.redshiftserverless.model.ListTagsForResourceResponse;
1315
import software.amazon.awssdk.services.redshiftserverless.model.ServiceQuotaExceededException;
1416
import software.amazon.awssdk.services.redshiftserverless.model.Namespace;
1517
import software.amazon.awssdk.services.redshiftserverless.model.ResourceNotFoundException;
18+
import software.amazon.awssdk.services.redshiftserverless.model.RedshiftServerlessResponse;
1619
import software.amazon.awssdk.services.redshiftserverless.model.TooManyTagsException;
1720
import software.amazon.awssdk.services.redshiftserverless.model.ValidationException;
1821
import software.amazon.awssdk.services.redshift.RedshiftClient;
@@ -35,6 +38,10 @@ public abstract class BaseHandlerStd extends BaseHandler<CallbackContext> {
3538
protected final String NAMESPACE_STATUS_AVAILABLE = "available";
3639
protected static final Constant BACKOFF_STRATEGY = Constant.of().
3740
timeout(Duration.ofMinutes(30L)).delay(Duration.ofSeconds(10L)).build();
41+
protected static final Constant PREOPERATION_BACKOFF_STRATEGY = Constant.of()
42+
.timeout(Duration.ofMinutes(5L))
43+
.delay(Duration.ofSeconds(5L))
44+
.build();
3845

3946
@Override
4047
public final ProgressEvent<ResourceModel, CallbackContext> handleRequest(
@@ -62,7 +69,11 @@ protected abstract ProgressEvent<ResourceModel, CallbackContext> handleRequest(
6269
final ProxyClient<SecretsManagerClient> secretsManagerProxyClient,
6370
final Logger logger);
6471

65-
protected boolean isNamespaceActive (final ProxyClient<RedshiftServerlessClient> proxyClient, ResourceModel resourceModel, CallbackContext context) {
72+
protected boolean isNamespaceActive (final Object awsRequest,
73+
final RedshiftServerlessResponse awsResponse,
74+
final ProxyClient<RedshiftServerlessClient> proxyClient,
75+
final ResourceModel resourceModel,
76+
final CallbackContext context) {
6677
GetNamespaceRequest getNamespaceRequest = GetNamespaceRequest.builder().namespaceName(resourceModel.getNamespaceName()).build();
6778
GetNamespaceResponse getNamespaceResponse = proxyClient.injectCredentialsAndInvokeV2(getNamespaceRequest, proxyClient.client()::getNamespace);
6879
Namespace namespace = getNamespaceResponse.namespace();
@@ -73,6 +84,15 @@ protected boolean isNamespaceActive (final ProxyClient<RedshiftServerlessClient>
7384
return NAMESPACE_STATUS_AVAILABLE.equalsIgnoreCase(getNamespaceResponse.namespace().statusAsString());
7485
}
7586

87+
protected ListTagsForResourceResponse readTags(final ListTagsForResourceRequest awsRequest,
88+
final ProxyClient<RedshiftServerlessClient> proxyClient) {
89+
90+
ListTagsForResourceResponse awsResponse = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::listTagsForResource);
91+
92+
logger.log(String.format("%s's tags have successfully been read.", ResourceModel.TYPE_NAME));
93+
return awsResponse;
94+
}
95+
7696
protected boolean isNamespaceActiveAfterDelete (final ProxyClient<RedshiftServerlessClient> proxyClient, ResourceModel resourceModel, CallbackContext context) {
7797
GetNamespaceRequest getNamespaceRequest = GetNamespaceRequest.builder().namespaceName(resourceModel.getNamespaceName()).build();
7898
try {

aws-redshiftserverless-namespace/src/main/java/software/amazon/redshiftserverless/namespace/CreateHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
4343
return proxy.initiate("AWS-RedshiftServerless-Namespace::Create", proxyClient, progress.getResourceModel(), callbackContext)
4444
.translateToServiceRequest(Translator::translateToCreateRequest)
4545
.makeServiceCall(this::createNamespace)
46-
.stabilize((_awsRequest, _awsResponse, _client, _model, _context) -> isNamespaceActive(_client, _model, _context))
46+
.stabilize(this::isNamespaceActive)
4747
.handleError(this::defaultErrorHandler)
4848
.done((_request, _response, _client, _model, _context) -> {
4949
callbackContext.setNamespaceArn(_response.namespace().namespaceArn());

aws-redshiftserverless-namespace/src/main/java/software/amazon/redshiftserverless/namespace/ReadHandler.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
5959
});
6060
return progress;
6161
})
62+
.then(progress -> {
63+
progress = proxy.initiate("AWS-RedshiftServerless-Namespace::Read::ReadTags", proxyClient, progress.getResourceModel(), progress.getCallbackContext())
64+
.translateToServiceRequest(Translator::translateToReadTagsRequest)
65+
.makeServiceCall(this::readTags)
66+
.handleError(this::defaultErrorHandler)
67+
.done((_request, _response, _client, _model, _context) -> {
68+
return ProgressEvent.progress(Translator.translateFromReadTagsResponse(_response, _model), _context);
69+
});
70+
return progress;
71+
})
6272
.then(progress -> {
6373
return proxy.initiate("AWS-Redshift-ResourcePolicy::Get", redshiftProxyClient, progress.getResourceModel(), callbackContext)
6474
.translateToServiceRequest(resourceModelRequest -> Translator.translateToGetResourcePolicy(resourceModelRequest, callbackContext.getNamespaceArn()))

aws-redshiftserverless-namespace/src/main/java/software/amazon/redshiftserverless/namespace/Translator.java

Lines changed: 92 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import com.fasterxml.jackson.core.JsonProcessingException;
55
import com.fasterxml.jackson.core.type.TypeReference;
66
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import com.google.gson.Gson;
8+
import com.google.gson.GsonBuilder;
79
import software.amazon.awssdk.awscore.AwsRequest;
810
import software.amazon.awssdk.services.redshift.model.DeleteResourcePolicyRequest;
911
import software.amazon.awssdk.services.redshift.model.GetResourcePolicyRequest;
@@ -19,6 +21,10 @@
1921
import software.amazon.awssdk.services.redshiftserverless.model.CreateSnapshotCopyConfigurationRequest;
2022
import software.amazon.awssdk.services.redshiftserverless.model.UpdateSnapshotCopyConfigurationRequest;
2123
import software.amazon.awssdk.services.redshiftserverless.model.DeleteSnapshotCopyConfigurationRequest;
24+
import software.amazon.awssdk.services.redshiftserverless.model.ListTagsForResourceRequest;
25+
import software.amazon.awssdk.services.redshiftserverless.model.ListTagsForResourceResponse;
26+
import software.amazon.awssdk.services.redshiftserverless.model.TagResourceRequest;
27+
import software.amazon.awssdk.services.redshiftserverless.model.UntagResourceRequest;
2228
import software.amazon.cloudformation.proxy.Logger;
2329

2430
import java.io.IOException;
@@ -38,6 +44,7 @@
3844
*/
3945

4046
public class Translator {
47+
private static final Gson GSON = new GsonBuilder().create();
4148

4249
/**
4350
* Request to create a resource
@@ -54,22 +61,13 @@ static CreateNamespaceRequest translateToCreateRequest(final ResourceModel model
5461
.defaultIamRoleArn(model.getDefaultIamRoleArn())
5562
.iamRoles(model.getIamRoles())
5663
.logExportsWithStrings(model.getLogExports())
57-
.tags(translateTagsToSdk(model.getTags()))
64+
.tags(translateToSdkTags(model.getTags()))
5865
.manageAdminPassword(model.getManageAdminPassword())
5966
.adminPasswordSecretKmsKeyId(model.getAdminPasswordSecretKmsKeyId())
6067
.redshiftIdcApplicationArn(model.getRedshiftIdcApplicationArn())
6168
.build();
6269
}
6370

64-
static List<software.amazon.awssdk.services.redshiftserverless.model.Tag> translateTagsToSdk(final List<software.amazon.redshiftserverless.namespace.Tag> tags) {
65-
return Optional.ofNullable(tags).orElse(Collections.emptyList())
66-
.stream()
67-
.map(tag -> software.amazon.awssdk.services.redshiftserverless.model.Tag.builder()
68-
.key(tag.getKey())
69-
.value(tag.getValue()).build())
70-
.collect(Collectors.toList());
71-
}
72-
7371
/*
7472
This function is to return the iam role in the same format as input iam roles.
7573
Instead of modifying the schema for backward compatibitlity we use regex to extract the iam role.
@@ -264,6 +262,90 @@ static AwsRequest untagResourceRequest(final ResourceModel model, final Set<Stri
264262
return awsRequest;
265263
}
266264

265+
/**
266+
* Request to read tags for a resource
267+
*
268+
* @param model resource model
269+
* @return awsRequest the aws service request to update tags of a resource
270+
*/
271+
static ListTagsForResourceRequest translateToReadTagsRequest(final ResourceModel model) {
272+
return ListTagsForResourceRequest.builder()
273+
.resourceArn(model.getNamespace().getNamespaceArn())
274+
.build();
275+
}
276+
277+
/**
278+
* Translates resource object from sdk into a resource model
279+
*
280+
* @param awsResponse the aws service describe resource response
281+
* @param model the resource model contained the current resource info
282+
* @return awsRequest the aws service request to update tags of a resource
283+
*/
284+
static ResourceModel translateFromReadTagsResponse(final ListTagsForResourceResponse awsResponse,
285+
final ResourceModel model) {
286+
return model.toBuilder()
287+
.tags(translateToModelTags(awsResponse.tags()))
288+
.build();
289+
}
290+
291+
/**
292+
* Request to update tags for a resource
293+
*
294+
* @param desiredResourceState the resource model request to update tags
295+
* @param currentResourceState the resource model request to delete tags
296+
* @return awsRequest the aws service request to update tags of a resource
297+
*/
298+
static UpdateTagsRequest translateToUpdateTagsRequest(final ResourceModel desiredResourceState,
299+
final ResourceModel currentResourceState) {
300+
String resourceArn = currentResourceState.getNamespace().getNamespaceArn();
301+
302+
List<Tag> toBeCreatedTags = desiredResourceState.getTags() == null ? Collections.emptyList() : desiredResourceState.getTags()
303+
.stream()
304+
.filter(tag -> currentResourceState.getTags() == null || !currentResourceState.getTags().contains(tag))
305+
.collect(Collectors.toList());
306+
307+
List<Tag> toBeDeletedTags = currentResourceState.getTags() == null ? Collections.emptyList() : currentResourceState.getTags()
308+
.stream()
309+
.filter(tag -> desiredResourceState.getTags() == null || !desiredResourceState.getTags().contains(tag))
310+
.collect(Collectors.toList());
311+
312+
return UpdateTagsRequest.builder()
313+
.createNewTagsRequest(TagResourceRequest.builder()
314+
.tags(translateToSdkTags(toBeCreatedTags))
315+
.resourceArn(resourceArn)
316+
.build())
317+
.deleteOldTagsRequest(UntagResourceRequest.builder()
318+
.tagKeys(toBeDeletedTags
319+
.stream()
320+
.map(Tag::getKey)
321+
.collect(Collectors.toList()))
322+
.resourceArn(resourceArn)
323+
.build())
324+
.build();
325+
}
326+
327+
private static software.amazon.awssdk.services.redshiftserverless.model.Tag translateToSdkTag(Tag tag) {
328+
return GSON.fromJson(GSON.toJson(tag), software.amazon.awssdk.services.redshiftserverless.model.Tag.class);
329+
}
330+
331+
private static List<software.amazon.awssdk.services.redshiftserverless.model.Tag> translateToSdkTags(final List<Tag> tags) {
332+
return tags == null ? null : tags
333+
.stream()
334+
.map(Translator::translateToSdkTag)
335+
.collect(Collectors.toList());
336+
}
337+
338+
private static Tag translateToModelTag(software.amazon.awssdk.services.redshiftserverless.model.Tag tag) {
339+
return GSON.fromJson(GSON.toJson(tag), Tag.class);
340+
}
341+
342+
private static List<Tag> translateToModelTags(Collection<software.amazon.awssdk.services.redshiftserverless.model.Tag> tags) {
343+
return tags == null ? null : tags
344+
.stream()
345+
.map(Translator::translateToModelTag)
346+
.collect(Collectors.toList());
347+
}
348+
267349
private static Namespace translateToModelNamespace(
268350
software.amazon.awssdk.services.redshiftserverless.model.Namespace namespace) {
269351

0 commit comments

Comments
 (0)