- * Converts a collection of Tag objects to a tag-name -> tag-value map.
- *
- * Note: Tag objects with null tag values will not be included in the output
- * map.
- *
- * @param tags Collection of tags to convert
- * @return Converted Map of tags
- */
- public static Map convertToMap(final Collection tags) {
- if (CollectionUtils.isEmpty(tags)) {
- return Collections.emptyMap();
- }
- return tags.stream()
- .filter(tag -> tag.getValue() != null)
- .collect(Collectors.toMap(
- software.amazon.qbusiness.application.Tag::getKey,
- software.amazon.qbusiness.application.Tag::getValue,
- (oldValue, newValue) -> newValue)
- );
- }
-
- /**
- * convertToSet
- *
- * Converts a tag map to a set of Tag objects.
- *
- * Note: Like convertToMap, convertToSet filters out value-less tag entries.
- *
- * @param tagMap Map of tags to convert
- * @return Set of Tag objects
- */
- public static Set convertToSet(final Map tagMap) {
- if (MapUtils.isEmpty(tagMap)) {
- return Collections.emptySet();
- }
- return tagMap.entrySet().stream()
- .filter(tag -> tag.getValue() != null)
- .map(tag -> Tag.builder()
- .key(tag.getKey())
- .value(tag.getValue())
- .build())
- .collect(Collectors.toSet());
- }
-
- public static List cfnTagsFromServiceTags(
- List serviceTags
- ) {
- return serviceTags.stream()
- .map(serviceTag -> new software.amazon.qbusiness.application.Tag(serviceTag.key(), serviceTag.value()))
- .toList();
- }
-
- public static List serviceTagsFromCfnTags(
- Collection modelTags,
- Map systemTags
- ) {
- if (modelTags == null && systemTags == null) {
- return null;
- }
-
- var tags = new ArrayList();
- if (modelTags != null) {
- for (software.amazon.qbusiness.application.Tag modelTag : modelTags) {
- tags.add(
- Tag.builder()
- .key(modelTag.getKey())
- .value(modelTag.getValue())
- .build()
- );
- }
- }
-
- if (systemTags != null) {
- for (Map.Entry systemTag: systemTags.entrySet()) {
- tags.add(Tag.builder()
- .key(systemTag.getKey())
- .value(systemTag.getValue()).build()
- );
- }
- }
-
- return tags;
- }
-
- /**
- * shouldUpdateTags
- *
- * Determines whether user defined tags have been changed during update.
- */
- public final boolean shouldUpdateTags(final ResourceHandlerRequest handlerRequest) {
- final Map previousTags = getPreviouslyAttachedTags(handlerRequest);
- final Map desiredTags = getNewDesiredTags(handlerRequest);
- return ObjectUtils.notEqual(previousTags, desiredTags);
- }
-
- /**
- * getPreviouslyAttachedTags
- *
- * If stack tags and resource tags are not merged together in Configuration class,
- * we will get previously attached system (with `aws:cloudformation` prefix) and user defined tags from
- * handlerRequest.getPreviousSystemTags() (system tags),
- * handlerRequest.getPreviousResourceTags() (stack tags),
- * handlerRequest.getPreviousResourceState().getTags() (resource tags).
- *
- * System tags are an optional feature. Merge them to your tags if you have enabled them for your resource.
- * System tags can change on resource update if the resource is imported to the stack.
- */
- public Map getPreviouslyAttachedTags(final ResourceHandlerRequest handlerRequest) {
- final Map previousTags = new HashMap<>();
-
- if (handlerRequest.getPreviousSystemTags() != null) {
- previousTags.putAll(handlerRequest.getPreviousSystemTags());
- }
-
- // get previous stack level tags from handlerRequest
- if (handlerRequest.getPreviousResourceTags() != null) {
- previousTags.putAll(handlerRequest.getPreviousResourceTags());
- }
-
- if (handlerRequest.getPreviousResourceState() != null && handlerRequest.getPreviousResourceState().getTags() != null) {
- previousTags.putAll(convertToMap(handlerRequest.getPreviousResourceState().getTags()));
- }
- return previousTags;
- }
-
- /**
- * getNewDesiredTags
- *
- * If stack tags and resource tags are not merged together in Configuration class,
- * we will get new desired system (with `aws:cloudformation` prefix) and user defined tags from
- * handlerRequest.getSystemTags() (system tags),
- * handlerRequest.getDesiredResourceTags() (stack tags),
- * handlerRequest.getDesiredResourceState().getTags() (resource tags).
- *
- * System tags are an optional feature. Merge them to your tags if you have enabled them for your resource.
- * System tags can change on resource update if the resource is imported to the stack.
- */
- public Map getNewDesiredTags(final ResourceHandlerRequest handlerRequest) {
- final Map desiredTags = new HashMap<>();
-
- if (handlerRequest.getSystemTags() != null) {
- desiredTags.putAll(handlerRequest.getSystemTags());
- }
-
- // get desired stack level tags from handlerRequest
- if (handlerRequest.getDesiredResourceTags() != null) {
- desiredTags.putAll(handlerRequest.getDesiredResourceTags());
- }
-
- desiredTags.putAll(convertToMap(handlerRequest.getDesiredResourceState().getTags())); // if tags are not null
- return desiredTags;
- }
-
- /**
- * generateTagsToAdd
- *
- * Determines the tags the customer desired to define or redefine.
- */
- public Map generateTagsToAdd(final Map previousTags, final Map desiredTags) {
- return desiredTags.entrySet().stream()
- .filter(e -> !previousTags.containsKey(e.getKey()) || !Objects.equals(previousTags.get(e.getKey()), e.getValue()))
- .collect(Collectors.toMap(
- Map.Entry::getKey,
- Map.Entry::getValue));
- }
-
- /**
- * getTagsToRemove
- *
- * Determines the tags the customer desired to remove from the function.
- */
- public Set generateTagsToRemove(final Map previousTags, final Map desiredTags) {
- final Set desiredTagNames = desiredTags.keySet();
-
- return previousTags.keySet().stream()
- .filter(tagName -> !desiredTagNames.contains(tagName))
- .collect(Collectors.toSet());
- }
}
diff --git a/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Translator.java b/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Translator.java
index 6a1ba3b..b583059 100644
--- a/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Translator.java
+++ b/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Translator.java
@@ -1,11 +1,14 @@
package software.amazon.qbusiness.application;
import java.time.Instant;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import software.amazon.awssdk.services.qbusiness.model.CreateApplicationRequest;
@@ -20,8 +23,8 @@
import software.amazon.awssdk.services.qbusiness.model.TagResourceRequest;
import software.amazon.awssdk.services.qbusiness.model.UntagResourceRequest;
import software.amazon.awssdk.services.qbusiness.model.UpdateApplicationRequest;
-import software.amazon.cloudformation.exceptions.CfnInvalidRequestException;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+import software.amazon.qbusiness.common.TagUtils;
/**
* This class is a centralized placeholder for
@@ -35,15 +38,17 @@ public class Translator {
/**
* Request to create a resource
*
- * @param model resource model
+ * @param request The CFN request
+ * @param model resource model
* @return awsRequest the aws service request to create a resource
*/
static CreateApplicationRequest translateToCreateRequest(
- final String idempotentToken,
- final ResourceModel model,
- final Map systemTags) {
+ final ResourceHandlerRequest request,
+ final ResourceModel model
+ ) {
+ var merged = TagUtils.mergeCreateHandlerTagsToSdkTags(cfnTagsToGenericMap(model.getTags()), request);
return CreateApplicationRequest.builder()
- .clientToken(idempotentToken)
+ .clientToken(request.getClientRequestToken())
.displayName(model.getDisplayName())
.roleArn(model.getRoleArn())
.identityType(model.getIdentityType())
@@ -53,7 +58,7 @@ static CreateApplicationRequest translateToCreateRequest(
.description(model.getDescription())
.encryptionConfiguration(toServiceEncryptionConfig(model.getEncryptionConfiguration()))
.attachmentsConfiguration(toServiceAttachmentConfiguration(model.getAttachmentsConfiguration()))
- .tags(TagHelper.serviceTagsFromCfnTags(model.getTags(), systemTags))
+ .tags(merged)
.qAppsConfiguration(toServiceQAppsConfiguration(model.getQAppsConfiguration()))
.personalizationConfiguration(toServicePersonalizationConfiguration(model.getPersonalizationConfiguration()))
.build();
@@ -217,7 +222,7 @@ static software.amazon.awssdk.services.qbusiness.model.PersonalizationConfigurat
}
static AutoSubscriptionConfiguration fromServiceAutoSubscriptionConfiguration(
- software.amazon.awssdk.services.qbusiness.model.AutoSubscriptionConfiguration serviceConfig
+ software.amazon.awssdk.services.qbusiness.model.AutoSubscriptionConfiguration serviceConfig
) {
if (serviceConfig == null) {
return null;
@@ -228,9 +233,9 @@ static AutoSubscriptionConfiguration fromServiceAutoSubscriptionConfiguration(
}
return AutoSubscriptionConfiguration.builder()
- .autoSubscribe(serviceConfig.autoSubscribeAsString())
- .defaultSubscriptionType(serviceConfig.defaultSubscriptionTypeAsString())
- .build();
+ .autoSubscribe(serviceConfig.autoSubscribeAsString())
+ .defaultSubscriptionType(serviceConfig.defaultSubscriptionTypeAsString())
+ .build();
}
static software.amazon.awssdk.services.qbusiness.model.AutoSubscriptionConfiguration toServiceAutoSubscriptionConfiguration(
@@ -252,10 +257,32 @@ static ResourceModel translateFromReadResponseWithTags(final ListTagsForResource
}
return model.toBuilder()
- .tags(TagHelper.cfnTagsFromServiceTags(listTagsResponse.tags()))
+ .tags(cfnTagsFromServiceTags(listTagsResponse.tags()))
.build();
}
+ static List cfnTagsFromServiceTags(
+ List serviceTags
+ ) {
+ return serviceTags.stream()
+ .map(serviceTag -> new software.amazon.qbusiness.application.Tag(serviceTag.key(), serviceTag.value()))
+ .toList();
+ }
+
+ public static Map cfnTagsToGenericMap(final Collection tags) {
+ if (CollectionUtils.isEmpty(tags)) {
+ return Map.of();
+ }
+
+ return tags.stream()
+ .filter(tag -> tag.getValue() != null)
+ .collect(Collectors.toMap(
+ software.amazon.qbusiness.application.Tag::getKey,
+ software.amazon.qbusiness.application.Tag::getValue,
+ (oldValue, newValue) -> newValue)
+ );
+ }
+
/**
* Request to delete a resource
*
@@ -290,9 +317,9 @@ static UpdateApplicationRequest translateToUpdateRequest(final ResourceModel mod
static UpdateApplicationRequest translateToPostCreateUpdateRequest(final ResourceModel model) {
return UpdateApplicationRequest.builder()
- .applicationId(model.getApplicationId())
- .autoSubscriptionConfiguration(toServiceAutoSubscriptionConfiguration(model.getAutoSubscriptionConfiguration()))
- .build();
+ .applicationId(model.getApplicationId())
+ .autoSubscriptionConfiguration(toServiceAutoSubscriptionConfiguration(model.getAutoSubscriptionConfiguration()))
+ .build();
}
/**
diff --git a/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/UpdateHandler.java b/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/UpdateHandler.java
index d10012f..f71548f 100644
--- a/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/UpdateHandler.java
+++ b/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/UpdateHandler.java
@@ -23,6 +23,7 @@
import software.amazon.cloudformation.proxy.ProxyClient;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
import software.amazon.cloudformation.proxy.delay.Constant;
+import software.amazon.qbusiness.common.TagUtils;
public class UpdateHandler extends BaseHandlerStd {
@@ -32,16 +33,14 @@ public class UpdateHandler extends BaseHandlerStd {
.build();
private final Constant backOffStrategy;
- private final TagHelper tagHelper;
private Logger logger;
public UpdateHandler() {
- this(DEFAULT_BACK_OFF_STRATEGY, new TagHelper());
+ this(DEFAULT_BACK_OFF_STRATEGY);
}
- public UpdateHandler(Constant backOffStrategy, TagHelper tagHelper) {
+ public UpdateHandler(Constant backOffStrategy) {
this.backOffStrategy = backOffStrategy;
- this.tagHelper = tagHelper;
}
protected ProgressEvent handleRequest(
@@ -72,14 +71,19 @@ protected ProgressEvent handleRequest(
.progress()
)
.then(progress -> {
- if (!tagHelper.shouldUpdateTags(request)) {
+ var previousTags = TagUtils.getPreviouslyAttachedTags(
+ Translator.cfnTagsToGenericMap(request.getPreviousResourceState().getTags()), request);
+ var desiredTags = TagUtils.getNewDesiredTags(
+ Translator.cfnTagsToGenericMap(request.getDesiredResourceState().getTags()), request);
+
+ if (!TagUtils.shouldUpdateTags(previousTags, desiredTags)) {
// No updates to tags needed, return early with get application. Since ReadHandler will return Done, this will be the last step
return readHandler(proxy, request, callbackContext, proxyClient, logger);
}
- Map tagsToAdd = tagHelper.generateTagsToAdd(
- tagHelper.getPreviouslyAttachedTags(request),
- tagHelper.getNewDesiredTags(request)
+ Map tagsToAdd = TagUtils.generateTagsToAdd(
+ previousTags,
+ desiredTags
);
if (tagsToAdd == null || tagsToAdd.isEmpty()) {
@@ -92,9 +96,9 @@ protected ProgressEvent handleRequest(
.progress();
})
.then(progress -> {
- Set tagsToRemove = tagHelper.generateTagsToRemove(
- tagHelper.getPreviouslyAttachedTags(request),
- tagHelper.getNewDesiredTags(request)
+ Set tagsToRemove = TagUtils.generateTagsToRemove(
+ TagUtils.getPreviouslyAttachedTags(Translator.cfnTagsToGenericMap(request.getPreviousResourceState().getTags()), request),
+ TagUtils.getNewDesiredTags(Translator.cfnTagsToGenericMap(request.getDesiredResourceState().getTags()), request)
);
if (CollectionUtils.isEmpty(tagsToRemove)) {
diff --git a/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Utils.java b/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Utils.java
index a7249c4..3e1a503 100644
--- a/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Utils.java
+++ b/aws-qbusiness-application/src/main/java/software/amazon/qbusiness/application/Utils.java
@@ -22,6 +22,7 @@ public static String buildApplicationArn(final ResourceHandlerRequest) t -> t.applicationId().equals(APP_ID))
);
verify(sdkClient).listTagsForResource(any(ListTagsForResourceRequest.class));
- verify(tagHelper).shouldUpdateTags(any());
}
private static Stream tagAndUntagArguments() {
@@ -299,7 +296,6 @@ proxy, testRequest, new CallbackContext(), proxyClient, logger
argThat((ArgumentMatcher) t -> t.applicationId().equals(APP_ID))
);
verify(sdkClient).listTagsForResource(any(ListTagsForResourceRequest.class));
- verify(tagHelper).shouldUpdateTags(any());
}
@Test
diff --git a/aws-qbusiness-common/pom.xml b/aws-qbusiness-common/pom.xml
new file mode 100644
index 0000000..8425195
--- /dev/null
+++ b/aws-qbusiness-common/pom.xml
@@ -0,0 +1,34 @@
+
+
+ 4.0.0
+
+ software.amazon.qbusiness.common
+ aws-qbusiness-handler-common
+ aws-qbusiness-common
+ 1.0
+ jar
+
+
+ 17
+ 17
+
+
+
+
+ software.amazon.qbusiness
+ aws-qbusiness-cloudformation-handlers
+ 1.0-SNAPSHOT
+
+
+
+
+
+ software.amazon.cloudformation
+ aws-cloudformation-rpdk-java-plugin
+ [2.0.0,3.0.0)
+
+
+
diff --git a/aws-qbusiness-common/src/main/java/software/amazon/qbusiness/common/SharedConstants.java b/aws-qbusiness-common/src/main/java/software/amazon/qbusiness/common/SharedConstants.java
new file mode 100644
index 0000000..1025825
--- /dev/null
+++ b/aws-qbusiness-common/src/main/java/software/amazon/qbusiness/common/SharedConstants.java
@@ -0,0 +1,11 @@
+package software.amazon.qbusiness.common;
+
+import java.util.Locale;
+
+public final class SharedConstants {
+ public static final String SERVICE_NAME = "QBusiness";
+ public static final String SERVICE_NAME_LOWER = SERVICE_NAME.toLowerCase(Locale.ENGLISH);
+ public static final String ENV_AWS_REGION = "AWS_REGION";
+
+ private SharedConstants(){}
+}
diff --git a/aws-qbusiness-common/src/main/java/software/amazon/qbusiness/common/TagUtils.java b/aws-qbusiness-common/src/main/java/software/amazon/qbusiness/common/TagUtils.java
new file mode 100644
index 0000000..08fc69c
--- /dev/null
+++ b/aws-qbusiness-common/src/main/java/software/amazon/qbusiness/common/TagUtils.java
@@ -0,0 +1,106 @@
+package software.amazon.qbusiness.common;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.commons.lang3.ObjectUtils;
+
+import software.amazon.awssdk.services.qbusiness.model.Tag;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+/**
+ * Class containing common handler tag operations.
+ */
+public final class TagUtils {
+
+ private TagUtils() {
+ }
+
+ public static List mergeCreateHandlerTagsToSdkTags(
+ final Map modelTags,
+ final ResourceHandlerRequest handlerRequest
+ ) {
+ return Stream.of(handlerRequest.getDesiredResourceTags(), modelTags, handlerRequest.getSystemTags())
+ .filter(Objects::nonNull)
+ .flatMap(map -> map.entrySet().stream())
+ .map(entry -> Tag.builder()
+ .key(entry.getKey())
+ .value(entry.getValue())
+ .build()
+ )
+ .collect(Collectors.toList());
+ }
+
+ public static boolean shouldUpdateTags(
+ final Map allPreviousTags,
+ final Map allDesiredTags
+ ) {
+ return ObjectUtils.notEqual(allPreviousTags, allDesiredTags);
+ }
+
+ public static Map getPreviouslyAttachedTags(
+ final Map previousModelTags,
+ final ResourceHandlerRequest handlerRequest
+ ) {
+ return mergedTags(
+ previousModelTags,
+ handlerRequest.getPreviousSystemTags(),
+ handlerRequest.getPreviousResourceTags()
+ );
+ }
+
+ public static Map getNewDesiredTags(
+ final Map modelTags,
+ final ResourceHandlerRequest handlerRequest
+ ) {
+ return mergedTags(
+ modelTags,
+ handlerRequest.getSystemTags(),
+ handlerRequest.getDesiredResourceTags()
+ );
+ }
+
+ public static Map generateTagsToAdd(
+ final Map previousTags,
+ final Map desiredTags
+ ) {
+ return desiredTags.entrySet().stream()
+ .filter(e -> !previousTags.containsKey(e.getKey()) || !Objects.equals(previousTags.get(e.getKey()), e.getValue()))
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ Map.Entry::getValue));
+ }
+
+ public static Set generateTagsToRemove(
+ final Map previousTags,
+ final Map desiredTags
+ ) {
+ final Set desiredTagNames = desiredTags.keySet();
+
+ return previousTags.keySet().stream()
+ .filter(tagName -> !desiredTagNames.contains(tagName))
+ .collect(Collectors.toSet());
+ }
+
+ private static Map mergedTags(
+ Map modelTags,
+ Map systemTags,
+ Map resourceTags
+ ) {
+ var combined = new HashMap();
+ Stream.of(
+ Optional.ofNullable(modelTags),
+ Optional.ofNullable(systemTags),
+ Optional.ofNullable(resourceTags)
+ ).flatMap(Optional::stream)
+ .forEach(combined::putAll);
+ return combined;
+ }
+
+}
diff --git a/aws-qbusiness-common/src/test/java/software/amazon/qbusiness/common/TagUtilsTest.java b/aws-qbusiness-common/src/test/java/software/amazon/qbusiness/common/TagUtilsTest.java
new file mode 100644
index 0000000..8b1618c
--- /dev/null
+++ b/aws-qbusiness-common/src/test/java/software/amazon/qbusiness/common/TagUtilsTest.java
@@ -0,0 +1,67 @@
+package software.amazon.qbusiness.common;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockitoAnnotations;
+
+import software.amazon.awssdk.services.qbusiness.model.Tag;
+import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
+
+class TagUtilsTest {
+
+ private ResourceHandlerRequest