Skip to content

Commit

Permalink
Add tag support to LogGroup resource (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
gruebel authored May 20, 2021
1 parent a0363cb commit 5e7acf4
Show file tree
Hide file tree
Showing 16 changed files with 989 additions and 59 deletions.
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));
}
}
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()),
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;
}
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

0 comments on commit 5e7acf4

Please sign in to comment.