Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tag support to LogGroup resource #53

Merged
merged 7 commits into from
May 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions aws-logs-loggroup/aws-logs-loggroup.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@
"typeName": "AWS::Logs::LogGroup",
"description": "Resource schema for AWS::Logs::LogGroup",
"sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-logs.git",
"definitions": {
"Tag": {
"description": "A key-value pair to associate with a resource.",
"type": "object",
"properties": {
"Key": {
"type": "string",
"description": "The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., :, /, =, +, - and @.",
"minLength": 1,
"maxLength": 128
},
"Value": {
"type": "string",
"description": "The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., :, /, =, +, - and @.",
"minLength": 0,
"maxLength": 256
}
},
"required": [
"Key",
"Value"
]
}
},
"properties": {
"LogGroupName": {
"description": "The name of the log group. If you don't specify a name, AWS CloudFormation generates a unique ID for the log group.",
Expand Down Expand Up @@ -39,6 +63,15 @@
3653
]
},
"Tags": {
"description": "An array of key-value pairs to apply to this resource.",
"type": "array",
"uniqueItems": true,
"insertionOrder": false,
"items": {
"$ref": "#/definitions/Tag"
}
},
"Arn": {
"description": "The CloudWatch log group ARN.",
"type": "string"
Expand All @@ -49,12 +82,14 @@
"permissions": [
"logs:DescribeLogGroups",
"logs:CreateLogGroup",
"logs:PutRetentionPolicy"
"logs:PutRetentionPolicy",
"logs:TagLogGroup"
]
},
"read": {
"permissions": [
"logs:DescribeLogGroups"
"logs:DescribeLogGroups",
"logs:ListTagsLogGroup"
]
},
"update": {
Expand All @@ -63,7 +98,9 @@
"logs:AssociateKmsKey",
"logs:DisassociateKmsKey",
"logs:PutRetentionPolicy",
"logs:DeleteRetentionPolicy"
"logs:DeleteRetentionPolicy",
"logs:TagLogGroup",
"logs:UntagLogGroup"
]
},
"delete": {
Expand All @@ -74,7 +111,8 @@
},
"list": {
"permissions": [
"logs:DescribeLogGroups"
"logs:DescribeLogGroups",
"logs:ListTagsLogGroup"
]
}
},
Expand Down
3 changes: 2 additions & 1 deletion aws-logs-loggroup/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<awssdk.version>2.15.19</awssdk.version>
</properties>

<repositories>
Expand Down Expand Up @@ -43,7 +44,7 @@
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>cloudwatchlogs</artifactId>
<version>2.13.18</version>
<version>${awssdk.version}</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.assertj/assertj-core -->
Expand Down
3 changes: 3 additions & 0 deletions aws-logs-loggroup/resource-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ Resources:
- "logs:DeleteRetentionPolicy"
- "logs:DescribeLogGroups"
- "logs:DisassociateKmsKey"
- "logs:ListTagsLogGroup"
- "logs:PutRetentionPolicy"
- "logs:TagLogGroup"
- "logs:UntagLogGroup"
Resource: "*"
Outputs:
ExecutionRoleArn:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.json.JSONObject;
import org.json.JSONTokener;
import software.amazon.awssdk.utils.CollectionUtils;

import java.util.Map;
import java.util.stream.Collectors;

class Configuration extends BaseConfiguration {

Expand All @@ -16,6 +18,12 @@ public JSONObject resourceSchemaJSONObject() {
}

public Map<String, String> resourceDefinedTags(final ResourceModel resourceModel) {
return null;
if (CollectionUtils.isNullOrEmpty(resourceModel.getTags())) {
return null;
}

return resourceModel.getTags()
.stream()
.collect(Collectors.toMap(Tag::getKey, Tag::getValue, (value1, value2) -> value2));
wbingli marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public ProgressEvent<ResourceModel, CallbackContext> handleRequest(
final ResourceModel model = request.getDesiredResourceState();

try {
proxy.injectCredentialsAndInvokeV2(Translator.translateToCreateRequest(model),
proxy.injectCredentialsAndInvokeV2(Translator.translateToCreateRequest(model, request.getDesiredResourceTags()),
wbingli marked this conversation as resolved.
Show resolved Hide resolved
ClientBuilder.getClient()::createLogGroup);
} catch (final ResourceAlreadyExistsException e) {
throw new CfnAlreadyExistsException(ResourceModel.TYPE_NAME,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package software.amazon.logs.loggroup;

import software.amazon.awssdk.services.cloudwatchlogs.model.ListTagsLogGroupResponse;
import software.amazon.awssdk.services.cloudwatchlogs.model.LogGroup;
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
import software.amazon.cloudformation.proxy.Logger;
import software.amazon.cloudformation.proxy.OperationStatus;
import software.amazon.cloudformation.proxy.ProgressEvent;
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeLogGroupsResponse;

import java.util.Map;
import java.util.stream.Collectors;

public class ListHandler extends BaseHandler<CallbackContext> {

@Override
Expand All @@ -19,9 +24,17 @@ public ProgressEvent<ResourceModel, CallbackContext> handleRequest(
final DescribeLogGroupsResponse response =
proxy.injectCredentialsAndInvokeV2(Translator.translateToListRequest(request.getNextToken()),
ClientBuilder.getClient()::describeLogGroups);

final Map<String, ListTagsLogGroupResponse> tagResponses = Translator.streamOfOrEmpty(response.logGroups())
.collect(Collectors.toMap(
LogGroup::logGroupName,
logGroup -> proxy.injectCredentialsAndInvokeV2(Translator.translateToListTagsLogGroupRequest(logGroup.logGroupName()),
ClientBuilder.getClient()::listTagsLogGroup))
);

return ProgressEvent.<ResourceModel, CallbackContext>builder()
.status(OperationStatus.SUCCESS)
.resourceModels(Translator.translateForList(response))
.resourceModels(Translator.translateForList(response, tagResponses))
.nextToken(response.nextToken())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package software.amazon.logs.loggroup;

import software.amazon.awssdk.services.cloudwatchlogs.model.CloudWatchLogsException;
import software.amazon.awssdk.services.cloudwatchlogs.model.ListTagsLogGroupResponse;
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
import software.amazon.cloudformation.proxy.Logger;
import software.amazon.cloudformation.proxy.ProgressEvent;
Expand All @@ -25,14 +27,26 @@ public ProgressEvent<ResourceModel, CallbackContext> handleRequest(
}

DescribeLogGroupsResponse response = null;
ListTagsLogGroupResponse tagsResponse = null;
try {
response = proxy.injectCredentialsAndInvokeV2(Translator.translateToReadRequest(model),
ClientBuilder.getClient()::describeLogGroups);
ClientBuilder.getClient()::describeLogGroups);
try {
tagsResponse = proxy.injectCredentialsAndInvokeV2(Translator.translateToListTagsLogGroupRequest(model.getLogGroupName()),
ClientBuilder.getClient()::listTagsLogGroup);
} catch (final CloudWatchLogsException e) {
if (Translator.ACCESS_DENIED_ERROR_CODE.equals(e.awsErrorDetails().errorCode())) {
// fail silently, if there is no permission to list tags
logger.log(e.getMessage());
} else {
throw e;
}
}
} catch (final ResourceNotFoundException e) {
throwNotFoundException(model);
}

final ResourceModel modelFromReadResult = Translator.translateForRead(response);
final ResourceModel modelFromReadResult = Translator.translateForRead(response, tagsResponse);
if (modelFromReadResult.getLogGroupName() == null) {
throwNotFoundException(model);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,29 @@
import software.amazon.awssdk.services.cloudwatchlogs.model.DeleteRetentionPolicyRequest;
import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeLogGroupsRequest;
import software.amazon.awssdk.services.cloudwatchlogs.model.DescribeLogGroupsResponse;
import software.amazon.awssdk.services.cloudwatchlogs.model.ListTagsLogGroupRequest;
import software.amazon.awssdk.services.cloudwatchlogs.model.ListTagsLogGroupResponse;
import software.amazon.awssdk.services.cloudwatchlogs.model.PutRetentionPolicyRequest;
import software.amazon.awssdk.services.cloudwatchlogs.model.DisassociateKmsKeyRequest;
import software.amazon.awssdk.services.cloudwatchlogs.model.AssociateKmsKeyRequest;
import software.amazon.awssdk.services.cloudwatchlogs.model.TagLogGroupRequest;
import software.amazon.awssdk.services.cloudwatchlogs.model.UntagLogGroupRequest;
import software.amazon.awssdk.utils.CollectionUtils;

import java.util.Collection;
import java.util.Collections;
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;

final class Translator {

static final String ACCESS_DENIED_ERROR_CODE = "AccessDeniedException";

private Translator() {}

static DescribeLogGroupsRequest translateToReadRequest(final ResourceModel model) {
Expand All @@ -38,10 +49,11 @@ static DeleteLogGroupRequest translateToDeleteRequest(final ResourceModel model)
.build();
}

static CreateLogGroupRequest translateToCreateRequest(final ResourceModel model) {
static CreateLogGroupRequest translateToCreateRequest(final ResourceModel model, final Map<String, String> tags) {
return CreateLogGroupRequest.builder()
.logGroupName(model.getLogGroupName())
.kmsKeyId(model.getKmsKeyId())
.tags(tags)
.build();
}

Expand Down Expand Up @@ -71,7 +83,27 @@ static AssociateKmsKeyRequest translateToAssociateKmsKeyRequest(final ResourceMo
.build();
}

static ResourceModel translateForRead(final DescribeLogGroupsResponse response) {
static ListTagsLogGroupRequest translateToListTagsLogGroupRequest(final String logGroupName) {
return ListTagsLogGroupRequest.builder()
.logGroupName(logGroupName)
.build();
}

static TagLogGroupRequest translateToTagLogGroupRequest(final String logGroupName, final Map<String, String> tags) {
return TagLogGroupRequest.builder()
.logGroupName(logGroupName)
.tags(tags)
.build();
}

static UntagLogGroupRequest translateToUntagLogGroupRequest(final String logGroupName, final List<String> tagKeys) {
return UntagLogGroupRequest.builder()
.logGroupName(logGroupName)
.tags(tagKeys)
.build();
}

static ResourceModel translateForRead(final DescribeLogGroupsResponse response, final ListTagsLogGroupResponse tagsResponse) {
final String logGroupName = streamOfOrEmpty(response.logGroups())
.map(software.amazon.awssdk.services.cloudwatchlogs.model.LogGroup::logGroupName)
.filter(Objects::nonNull)
Expand All @@ -92,26 +124,31 @@ static ResourceModel translateForRead(final DescribeLogGroupsResponse response)
.filter(Objects::nonNull)
.findAny()
.orElse(null);
final Set<Tag> tags = translateSdkToTags(Optional.ofNullable(tagsResponse)
.map(ListTagsLogGroupResponse::tags)
.orElse(null));
return ResourceModel.builder()
.arn(logGroupArn)
.logGroupName(logGroupName)
.retentionInDays(retentionInDays)
.kmsKeyId(kmsKeyId)
.tags(tags)
.build();
}

static List<ResourceModel> translateForList(final DescribeLogGroupsResponse response) {
static List<ResourceModel> translateForList(final DescribeLogGroupsResponse response, final Map<String, ListTagsLogGroupResponse> tagResponses) {
return streamOfOrEmpty(response.logGroups())
.map(logGroup -> ResourceModel.builder()
.arn(logGroup.arn())
.logGroupName(logGroup.logGroupName())
.retentionInDays(logGroup.retentionInDays())
.kmsKeyId(logGroup.kmsKeyId())
.tags(translateSdkToTags(tagResponses.get(logGroup.logGroupName()).tags()))
.build())
.collect(Collectors.toList());
}

private static <T> Stream<T> streamOfOrEmpty(final Collection<T> collection) {
static <T> Stream<T> streamOfOrEmpty(final Collection<T> collection) {
return Optional.ofNullable(collection)
.map(Collection::stream)
.orElseGet(Stream::empty);
Expand All @@ -128,4 +165,19 @@ static String buildResourceDoesNotExistErrorMessage(final String resourceIdentif
ResourceModel.TYPE_NAME,
resourceIdentifier);
}

static Map<String, String> translateTagsToSdk(final Set<Tag> tags) {
if (CollectionUtils.isNullOrEmpty(tags)) {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of returning null can we return an empty map?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I don't return null here some of the contract tests will fail with

{'status': 'FAILED', 'errorCode': 'GeneralServiceException', 'message': "1 validation error detected: Value '{}' at 'tags' failed to satisfy constraint: Member must have length greater than or equal to 1 (Service: CloudWatchLogs, Status Code: 400, Request ID: 710f70c1-0dad-4f89-91ad-3eadd83b6e9c, Extended Request ID: null)", 'callbackDelaySeconds': 0}

}
return tags.stream().collect(Collectors.toMap(Tag::getKey, Tag::getValue));
}

static Set<Tag> translateSdkToTags(final Map<String, String> tags) {
if (CollectionUtils.isNullOrEmpty(tags)) {
return null;
}
return tags.entrySet().stream().map(tag -> new Tag(tag.getKey(), tag.getValue()))
.collect(Collectors.toSet());
}
}
Loading