Thanks to visit codestin.com
Credit goes to github.com

Skip to content

implement resource tag api for cloudwatch logs #7389

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

Merged
merged 2 commits into from
Dec 24, 2022
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
19 changes: 19 additions & 0 deletions localstack/services/logs/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Dict

from moto.logs.models import LogsBackend as MotoLogsBackend
from moto.logs.models import logs_backends as moto_logs_backend

from localstack.services.stores import AccountRegionBundle, BaseStore, CrossRegionAttribute


def get_moto_logs_backend(account_id: str, region_name: str) -> MotoLogsBackend:
return moto_logs_backend[account_id][region_name]


class LogsStore(BaseStore):

# maps resource ARN to tags
TAGS: Dict[str, Dict[str, str]] = CrossRegionAttribute(default=dict)


logs_stores = AccountRegionBundle("logs", LogsStore)
106 changes: 106 additions & 0 deletions localstack/services/logs/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@
from localstack.aws.accounts import get_aws_account_id
from localstack.aws.api import RequestContext
from localstack.aws.api.logs import (
AmazonResourceName,
InputLogEvents,
KmsKeyId,
ListTagsForResourceResponse,
ListTagsLogGroupResponse,
LogGroupName,
LogsApi,
LogStreamName,
PutLogEventsResponse,
SequenceToken,
TagKeyList,
TagList,
Tags,
)
from localstack.services.logs.models import get_moto_logs_backend, logs_stores
from localstack.services.moto import call_moto
from localstack.services.plugins import ServiceLifecycleHook
from localstack.utils.aws import arns, aws_stack
Expand Down Expand Up @@ -72,6 +80,104 @@ def put_log_events(
)
return call_moto(context)

def create_log_group(
self,
context: RequestContext,
log_group_name: LogGroupName,
kms_key_id: KmsKeyId = None,
tags: Tags = None,
) -> None:
call_moto(context)
if tags:
resource_arn = arns.log_group_arn(
group_name=log_group_name, account_id=context.account_id, region_name=context.region
)
store = logs_stores[context.account_id][context.region]
store.TAGS.setdefault(resource_arn, {}).update(tags)

def list_tags_for_resource(
self, context: RequestContext, resource_arn: AmazonResourceName
) -> ListTagsForResourceResponse:
self._check_resource_arn_tagging(resource_arn)
store = logs_stores[context.account_id][context.region]
tags = store.TAGS.get(resource_arn, {})
return ListTagsForResourceResponse(tags=tags)

def list_tags_log_group(
self, context: RequestContext, log_group_name: LogGroupName
) -> ListTagsLogGroupResponse:
# deprecated implementation, new one: list_tags_for_resource
self._verify_log_group_exists(
group_name=log_group_name, account_id=context.account_id, region_name=context.region
)
resource_arn = arns.log_group_arn(
group_name=log_group_name, account_id=context.account_id, region_name=context.region
)
store = logs_stores[context.account_id][context.region]
tags = store.TAGS.get(resource_arn, {})
return ListTagsLogGroupResponse(tags=tags)

def untag_resource(
self, context: RequestContext, resource_arn: AmazonResourceName, tag_keys: TagKeyList
) -> None:
self._check_resource_arn_tagging(resource_arn)
store = logs_stores[context.account_id][context.region]
tags_stored = store.TAGS.get(resource_arn, {})
for tag in tag_keys:
tags_stored.pop(tag, None)

def untag_log_group(
self, context: RequestContext, log_group_name: LogGroupName, tags: TagList
) -> None:
# deprecated implementation -> new one: untag_resource
self._verify_log_group_exists(
group_name=log_group_name, account_id=context.account_id, region_name=context.region
)
resource_arn = arns.log_group_arn(
group_name=log_group_name, account_id=context.account_id, region_name=context.region
)
store = logs_stores[context.account_id][context.region]
tags_stored = store.TAGS.get(resource_arn, {})
for tag in tags:
tags_stored.pop(tag, None)

def tag_resource(
self, context: RequestContext, resource_arn: AmazonResourceName, tags: Tags
) -> None:
self._check_resource_arn_tagging(resource_arn)
store = logs_stores[context.account_id][context.region]
store.TAGS.get(resource_arn, {}).update(tags or {})

def tag_log_group(
self, context: RequestContext, log_group_name: LogGroupName, tags: Tags
) -> None:
# deprecated implementation -> new one: tag_resource
self._verify_log_group_exists(
group_name=log_group_name, account_id=context.account_id, region_name=context.region
)
resource_arn = arns.log_group_arn(
group_name=log_group_name, account_id=context.account_id, region_name=context.region
)
store = logs_stores[context.account_id][context.region]
store.TAGS.get(resource_arn, {}).update(tags or {})

def _verify_log_group_exists(self, group_name: LogGroupName, account_id: str, region_name: str):
store = get_moto_logs_backend(account_id, region_name)
if group_name not in store.groups:
raise ResourceNotFoundException()

def _check_resource_arn_tagging(self, resource_arn):
service = arns.extract_service_from_arn(resource_arn)
region = arns.extract_region_from_arn(resource_arn)
account = arns.extract_account_id_from_arn(resource_arn)

# AWS currently only supports tagging for Log Group and Destinations
# LS: we only verify if log group exists, and create tags for other resources
if service.lower().startswith("log-group:"):
self._verify_log_group_exists(
service.split(":")[-1], account_id=account, region_name=region
)


def get_pattern_matcher(pattern: str) -> Callable[[str, Dict], bool]:
"""Returns a pattern matcher. Can be patched by plugins to return a more sophisticated pattern matcher."""
Expand Down
55 changes: 47 additions & 8 deletions tests/integration/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,56 @@ def test_create_and_delete_log_group(self, logs_client):
)
assert len(log_groups_after) == len(log_groups_before)

def test_list_tags_log_group(self, logs_client):
@pytest.mark.aws_validated
def test_list_tags_log_group(self, logs_client, snapshot):
test_name = f"test-log-group-{short_uid()}"
logs_client.create_log_group(logGroupName=test_name, tags={"env": "testing1"})
try:
logs_client.create_log_group(logGroupName=test_name, tags={"env": "testing1"})
response = logs_client.list_tags_log_group(logGroupName=test_name)
snapshot.match("list_tags_after_create_log_group", response)

# get group arn, to use the tag-resource api
log_group_arn = logs_client.describe_log_groups(logGroupNamePrefix=test_name)[
"logGroups"
][0]["arn"].rstrip(":*")

# add a tag - new api
logs_client.tag_resource(
resourceArn=log_group_arn, tags={"test1": "val1", "test2": "val2"}
)

response = logs_client.list_tags_log_group(logGroupName=test_name)
assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
assert "tags" in response
assert response["tags"]["env"] == "testing1"
response = logs_client.list_tags_log_group(logGroupName=test_name)
response_2 = logs_client.list_tags_for_resource(resourceArn=log_group_arn)

# clean up
logs_client.delete_log_group(logGroupName=test_name)
snapshot.match("list_tags_log_group_after_tag_resource", response)
snapshot.match("list_tags_for_resource_after_tag_resource", response_2)
# values should be the same
assert response["tags"] == response_2["tags"]

# add a tag - old api
logs_client.tag_log_group(logGroupName=test_name, tags={"test3": "val3"})

response = logs_client.list_tags_log_group(logGroupName=test_name)
response_2 = logs_client.list_tags_for_resource(resourceArn=log_group_arn)

snapshot.match("list_tags_log_group_after_tag_log_group", response)
snapshot.match("list_tags_for_resource_after_tag_log_group", response_2)
assert response["tags"] == response_2["tags"]

# untag - use both apis
logs_client.untag_log_group(logGroupName=test_name, tags=["test3"])
logs_client.untag_resource(resourceArn=log_group_arn, tagKeys=["env", "test1"])

response = logs_client.list_tags_log_group(logGroupName=test_name)
response_2 = logs_client.list_tags_for_resource(resourceArn=log_group_arn)
snapshot.match("list_tags_log_group_after_untag", response)
snapshot.match("list_tags_for_resource_after_untag", response_2)

assert response["tags"] == response_2["tags"]

finally:
# clean up
logs_client.delete_log_group(logGroupName=test_name)

def test_create_and_delete_log_stream(self, logs_client, logs_log_group):
test_name = f"test-log-stream-{short_uid()}"
Expand Down
78 changes: 78 additions & 0 deletions tests/integration/test_logs.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,83 @@
}
]
}
},
"tests/integration/test_logs.py::TestCloudWatchLogs::test_list_tags_log_group": {
"recorded-date": "22-12-2022, 17:46:54",
"recorded-content": {
"list_tags_after_create_log_group": {
"tags": {
"env": "testing1"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"list_tags_log_group_after_tag_resource": {
"tags": {
"env": "testing1",
"test1": "val1",
"test2": "val2"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"list_tags_for_resource_after_tag_resource": {
"tags": {
"env": "testing1",
"test1": "val1",
"test2": "val2"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"list_tags_log_group_after_tag_log_group": {
"tags": {
"env": "testing1",
"test1": "val1",
"test2": "val2",
"test3": "val3"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"list_tags_for_resource_after_tag_log_group": {
"tags": {
"env": "testing1",
"test1": "val1",
"test2": "val2",
"test3": "val3"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"list_tags_log_group_after_untag": {
"tags": {
"test2": "val2"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"list_tags_for_resource_after_untag": {
"tags": {
"test2": "val2"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
}
}
}
}