Skip to content

Commit

Permalink
Use common project constant and Tag Utils (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
jregistr authored Dec 17, 2024
1 parent 3ceec5a commit 4a42521
Show file tree
Hide file tree
Showing 19 changed files with 365 additions and 228 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

This repository contains CloudFormation resource providers for the `AWS::QBusiness::*` namespace.

## Build and Test your changes
#### Compile and run the tests for all resources
```shell
mvn verify
```
#### Compile and run tests for a specific resource
```shell
mvn verify -pl aws-qbusiness-application -am
```

## Security

Expand Down
17 changes: 14 additions & 3 deletions aws-qbusiness-application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
<modelVersion>4.0.0</modelVersion>

<groupId>software.amazon.qbusiness.application</groupId>
<artifactId>aws-qbusiness-application-handler</artifactId>
<name>aws-qbusiness-application-handler</name>
<version>1.0-SNAPSHOT</version>
<artifactId>aws-qbusiness-application</artifactId>
<name>aws-qbusiness-application</name>
<version>1.0</version>
<packaging>jar</packaging>

<parent>
Expand All @@ -26,6 +26,17 @@
</properties>

<dependencies>
<dependency>
<groupId>software.amazon.qbusiness.common</groupId>
<artifactId>aws-qbusiness-handler-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/software.amazon.cloudformation/aws-cloudformation-rpdk-java-plugin -->
<dependency>
<groupId>software.amazon.cloudformation</groupId>
<artifactId>aws-cloudformation-rpdk-java-plugin</artifactId>
<version>[2.0.0,3.0.0)</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package software.amazon.qbusiness.application;

import static software.amazon.qbusiness.application.Constants.ENV_AWS_REGION;
import static software.amazon.qbusiness.application.Constants.SERVICE_NAME_LOWER;
import static software.amazon.qbusiness.common.SharedConstants.ENV_AWS_REGION;
import static software.amazon.qbusiness.common.SharedConstants.SERVICE_NAME_LOWER;

import java.net.URI;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import software.amazon.awssdk.services.qbusiness.model.CreateApplicationResponse;
import software.amazon.awssdk.services.qbusiness.model.GetApplicationResponse;
import software.amazon.awssdk.services.qbusiness.model.IdentityType;
import software.amazon.awssdk.services.qbusiness.model.SubscriptionType;
import software.amazon.awssdk.services.qbusiness.model.UpdateApplicationRequest;
import software.amazon.awssdk.services.qbusiness.model.UpdateApplicationResponse;
import software.amazon.awssdk.utils.StringUtils;
Expand Down Expand Up @@ -59,7 +58,7 @@ protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext)
.then(progress ->
proxy.initiate("AWS-QBusiness-Application::Create", proxyClient, progress.getResourceModel(), progress.getCallbackContext())
.translateToServiceRequest(model -> Translator.translateToCreateRequest(request.getClientRequestToken(), model, request.getSystemTags()))
.translateToServiceRequest(model -> Translator.translateToCreateRequest(request, model))
.backoffDelay(backOffStrategy)
.makeServiceCall((awsRequest, clientProxyClient) -> callCreateApplication(awsRequest, clientProxyClient, progress.getResourceModel()))
.stabilize((awsReq, response, clientProxyClient, model, context) -> isStabilized(clientProxyClient, model, logger))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,187 +18,6 @@
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;

public class TagHelper {
/**
* convertToMap
* <p>
* Converts a collection of Tag objects to a tag-name -> tag-value map.
* <p>
* 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<String, String> convertToMap(final Collection<software.amazon.qbusiness.application.Tag> 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
* <p>
* Converts a tag map to a set of Tag objects.
* <p>
* 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<Tag> convertToSet(final Map<String, String> 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<software.amazon.qbusiness.application.Tag> cfnTagsFromServiceTags(
List<Tag> serviceTags
) {
return serviceTags.stream()
.map(serviceTag -> new software.amazon.qbusiness.application.Tag(serviceTag.key(), serviceTag.value()))
.toList();
}

public static List<Tag> serviceTagsFromCfnTags(
Collection<software.amazon.qbusiness.application.Tag> modelTags,
Map<String, String> systemTags
) {
if (modelTags == null && systemTags == null) {
return null;
}

var tags = new ArrayList<Tag>();
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<String, String> systemTag: systemTags.entrySet()) {
tags.add(Tag.builder()
.key(systemTag.getKey())
.value(systemTag.getValue()).build()
);
}
}

return tags;
}

/**
* shouldUpdateTags
* <p>
* Determines whether user defined tags have been changed during update.
*/
public final boolean shouldUpdateTags(final ResourceHandlerRequest<ResourceModel> handlerRequest) {
final Map<String, String> previousTags = getPreviouslyAttachedTags(handlerRequest);
final Map<String, String> desiredTags = getNewDesiredTags(handlerRequest);
return ObjectUtils.notEqual(previousTags, desiredTags);
}

/**
* getPreviouslyAttachedTags
* <p>
* 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).
* <p>
* 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<String, String> getPreviouslyAttachedTags(final ResourceHandlerRequest<ResourceModel> handlerRequest) {
final Map<String, String> 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
* <p>
* 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).
* <p>
* 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<String, String> getNewDesiredTags(final ResourceHandlerRequest<ResourceModel> handlerRequest) {
final Map<String, String> 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
* <p>
* Determines the tags the customer desired to define or redefine.
*/
public Map<String, String> generateTagsToAdd(final Map<String, String> previousTags, final Map<String, String> 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
* <p>
* Determines the tags the customer desired to remove from the function.
*/
public Set<String> generateTagsToRemove(final Map<String, String> previousTags, final Map<String, String> desiredTags) {
final Set<String> desiredTagNames = desiredTags.keySet();

return previousTags.keySet().stream()
.filter(tagName -> !desiredTagNames.contains(tagName))
.collect(Collectors.toSet());
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand All @@ -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<String, String> systemTags) {
final ResourceHandlerRequest<ResourceModel> 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())
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand All @@ -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(
Expand All @@ -252,10 +257,32 @@ static ResourceModel translateFromReadResponseWithTags(final ListTagsForResource
}

return model.toBuilder()
.tags(TagHelper.cfnTagsFromServiceTags(listTagsResponse.tags()))
.tags(cfnTagsFromServiceTags(listTagsResponse.tags()))
.build();
}

static List<software.amazon.qbusiness.application.Tag> cfnTagsFromServiceTags(
List<Tag> serviceTags
) {
return serviceTags.stream()
.map(serviceTag -> new software.amazon.qbusiness.application.Tag(serviceTag.key(), serviceTag.value()))
.toList();
}

public static Map<String, String> cfnTagsToGenericMap(final Collection<software.amazon.qbusiness.application.Tag> 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
*
Expand Down Expand Up @@ -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();
}

/**
Expand Down
Loading

0 comments on commit 4a42521

Please sign in to comment.