8000 add: TaggingService functionality to Lambda · localstack/localstack@e7e4c11 · GitHub
[go: up one dir, main page]

Skip to content

Commit e7e4c11

Browse files
committed
add: TaggingService functionality to Lambda
1 parent 3a54f21 commit e7e4c11

File tree

6 files changed

+647
-83
lines changed

6 files changed

+647
-83
lines changed

localstack-core/localstack/services/lambda_/api_utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@
108108
# pattern therefore we can sub this value in when appropriate.
109109
ARN_NAME_PATTERN_VALIDATION_TEMPLATE = "(arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{{2}}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{{1}}:)?(\\d{{12}}:)?(function:)?([a-zA-Z0-9-_{0}]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?"
110110

111+
TAGGABLE_RESOURCE_ARN_PATTERN = "arn:(aws[a-zA-Z-]*):lambda:[a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:\\d{12}:(function:[a-zA-Z0-9-_]+(:(\\$LATEST|[a-zA-Z0-9-_]+))?|layer:([a-zA-Z0-9-_]+)|code-signing-config:csc-[a-z0-9]{17}|event-source-mapping:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})"
112+
111113

112114
def validate_function_name(function_name_or_arn: str, operation_type: str):
113115
function_name, *_ = function_locators_from_arn(function_name_or_arn)

localstack-core/localstack/services/lambda_/invocation/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from localstack.aws.api.lambda_ import EventSourceMappingConfiguration
22
from localstack.services.lambda_.invocation.lambda_models import CodeSigningConfig, Function, Layer
33
from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
4+
from localstack.utils.tagging import TaggingService
45

56

67
class LambdaStore(BaseStore):
@@ -16,5 +17,8 @@ class LambdaStore(BaseStore):
1617
# maps layer names to Layers
1718
layers: dict[str, Layer] = LocalAttribute(default=dict)
1819

20+
# maps resource ARNs for EventSourceMappings and CodeSigningConfiguration to tags
21+
TAGS = LocalAttribute(default=TaggingService)
22+
1923

2024
lambda_stores = AccountRegionBundle("lambda", LambdaStore)

localstack-core/localstack/services/lambda_/provider.py

Lines changed: 117 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
Description,
4242
DestinationConfig,
4343
EventSourceMappingConfiguration,
44-
FunctionArn,
4544
FunctionCodeLocation,
4645
FunctionConfiguration,
4746
FunctionEventInvokeConfig,
@@ -127,6 +126,7 @@
127126
StatementId,
128127
StateReasonCode,
129128
String,
129+
TaggableResource,
130130
TagKeyList,
131131
Tags,
132132
TracingMode,
@@ -221,8 +221,12 @@
221221
from localstack.services.plugins import ServiceLifecycleHook
222222
from localstack.state import StateVisitor
223223
from localstack.utils.aws.arns import (
224+
ArnData,
225+
extract_resource_from_arn,
224226
extract_service_from_arn,
225227
get_partition,
228+
lambda_event_source_mapping_arn,
229+
parse_arn,
226230
)
227231
from localstack.utils.bootstrap import is_api_enabled
228232
from localstack.utils.collections import PaginatedList
@@ -394,6 +398,18 @@ def _get_function(function_name: str, account_id: str, region: str) -> Function:
394398
)
395399
return function
396400

401+
@staticmethod
402+
def _get_esm(uuid: str, account_id: str, region: str) -> EventSourceMappingConfiguration:
403+
state = lambda_stores[account_id][region]
404+
esm = state.event_source_mappings.get(uuid)
405+
if not esm:
406+
arn = lambda_event_source_mapping_arn(uuid, account_id, region)
407+
raise ResourceNotFoundException(
408+
f"Event source mapping not found: {arn}",
409+
Type="User",
410+
)
411+
return esm
412+
397413
@staticmethod
398414
def _validate_qualifier_expression(qualifier: str) -> None:
399415
if error_messages := api_utils.validate_qualifier(qualifier):
@@ -988,7 +1004,7 @@ def create_function(
9881004
)
9891005
fn.versions["$LATEST"] = version
9901006
if request.get("Tags"):
991-
self._store_tags(fn, request["Tags"])
1007+
self._store_tags(arn.unqualified_arn(), request["Tags"])
9921008
# TODO: should validation failures here "fail" the function creation like it is now?
9931009
state.functions[function_name] = fn
9941010
self.lambda_service.create_function_version(version)
@@ -1453,7 +1469,7 @@ def get_function(
14531469
account_id=account_id,
14541470
region=region,
14551471
)
1456-
tags = self._get_tags(fn)
1472+
tags = self._get_tags(api_utils.unqualified_lambda_arn(function_name, account_id, region))
14571473
additional_fields = {}
14581474
if tags:
14591475
additional_fields["Tags"] = tags
@@ -1873,6 +1889,8 @@ def create_event_source_mapping_v2(
18731889
esm_worker = EsmWorkerFactory(esm_config, function_role, enabled).get_esm_worker()
18741890
self.esm_workers[esm_worker.uuid] = esm_worker
18751891
# TODO: check StateTransitionReason, LastModified, LastProcessingResult (concurrent updates requires locking!)
1892+
if tags := request.get("Tags"):
1893+
self._store_tags(esm_config.get("EventSourceMappingArn"), tags)
18761894
esm_worker.create()
18771895
return esm_config
18781896

@@ -4118,87 +4136,129 @@ def delete_function_concurrency(
41184136
# =======================================
41194137
# =============== TAGS ===============
41204138
# =======================================
4121-
# only function ARNs are available for tagging
4139+
# only Function, Event Source Mapping, and Code Signing Config (not currently supported by LocalStack) ARNs an are available for tagging in AWS
41224140

4123-
def _get_tags(self, function: Function) -> dict[str, str]:
4124-
return function.tags or {}
4141+
def _get_tags(self, resource: TaggableResource) -> dict[str, str]:
4142+
state = self.fetch_lambda_store_for_tagging(resource)
4143+
lambda_adapted_tags = {
4144+
tag["Key"]: tag["Value"]
4145+
for tag in state.TAGS.list_tags_for_resource(resource).get("Tags")
4146+
}
4147+
return lambda_adapted_tags
41254148

4126-
def _store_tags(self, function: Function, tags: dict[str, str]):
4127-
if len(tags) > LAMBDA_TAG_LIMIT_PER_RESOURCE:
4149+
def _store_tags(self, resource: TaggableResource, tags: dict[str, str]):
4150+
state = self.fetch_lambda_store_for_tagging(resource)
4151+
if len(state.TAGS.tags.get(resource, {}) | tags) > LAMBDA_TAG_LIMIT_PER_RESOURCE:
41284152
raise InvalidParameterValueException(
41294153
"Number of tags exceeds resource tag limit.", Type="User"
41304154
)
4131-
with function.lock:
4132-
function.tags = tags
4133-
# dirty hack for changed revision id, should reevaluate model to prevent this:
4134-
latest_version = function.versions["$LATEST"]
4135-
function.versions["$LATEST"] = dataclasses.replace(
4136-
latest_version, config=dataclasses.replace(latest_version.config)
4155+
4156+
tag_svc_adapted_tags = [{"Key": key, "Value": value} for key, value in tags.items()]
4157+
state.TAGS.tag_resource(resource, tag_svc_adapted_tags)
4158+
4159+
def fetch_lambda_store_for_tagging(self, resource: TaggableResource) -> LambdaStore:
4160+
def _raise_validation_exception():
4161+
raise ValidationException(
4162+
f"1 validation error detected: Value '{resource}' at 'resource' failed to satisfy constraint: Member must satisfy regular expression pattern: {api_utils.TAGGABLE_RESOURCE_ARN_PATTERN}"
41374163
)
41384164

4139-
def _update_tags(self, function: Function, tags: dict[str, str]):
4140-
with function.lock:
4141-
stored_tags = function.tags or {}
4142-
stored_tags |= tags
4143-
self._store_tags(function=function, tags=stored_tags)
4165+
# Check whether the ARN we have been passed is correctly formatted
4166+
parsed_resource_arn: ArnData = None
4167+
try:
4168+
parsed_resource_arn = parse_arn(resource)
4169+
except Exception:
4170+
_raise_validation_exception()
4171+
4172+
# TODO: Should we be checking whether this is a full ARN?
4173+
region, account_id, resource_type = map(
4174+
parsed_resource_arn.get, ("region", "account", "resource")
4175+
)
4176+
4177+
if not all((region, account_id, resource_type)):
4178+
_raise_validation_exception()
4179+
4180+
if not (parts := resource_type.split(":")):
4181+
_raise_validation_exception()
4182+
4183+
resource_type, resource_identifier, *qualifier = parts
4184+
if resource_type not in {"event-source-mapping", "code-signing-config", "function"}:
4185+
_raise_validation_exception()
4186+
4187+
if qualifier:
4188+
if resource_type == "function":
4189+
raise InvalidParameterValueException(
4190+
"Tags on function aliases and versions are not supported. Please specify a function ARN.",
4191+
Type="User",
4192+
)
4193+
_raise_validation_exception()
4194+
4195+
match resource_type:
4196+
case "event-source-mapping":
4197+
self._get_esm(resource_identifier, account_id, region)
4198+
case "code-signing-config":
4199+
raise NotImplementedError("Resource tagging on CSC not yet implemented.")
4200+
case "function":
4201+
self._get_function(
4202+
function_name=resource_identifier, account_id=account_id, region=region
4203+
)
4204+
4205+
# If no exceptions are raised, assume ARN and referenced resource is valid for tag operations
4206+
return lambda_stores[account_id][region]
41444207

41454208
def tag_resource(
4146-
self, context: RequestContext, resource: FunctionArn, tags: Tags, **kwargs
4209+
self, context: RequestContext, resource: TaggableResource, tags: Tags, **kwargs
41474210
) -> None:
41484211
if not tags:
41494212
raise InvalidParameterValueException(
41504213
"An error occurred and the request cannot be processed.", Type="User"
41514214
)
4215+
self._store_tags(resource, tags)
41524216

4153-
# TODO: test layer (added in snapshot update 2023-11)
4154-
pattern_match = api_utils.FULL_FN_ARN_PATTERN.search(resource)
4155-
if not pattern_match:
4156-
raise ValidationException(
4157-
rf"1 validation error detected: Value '{resource}' at 'resource' failed to satisfy constraint: Member must satisfy regular expression pattern: arn:(aws[a-zA-Z-]*)?:lambda:[a-z]{{2}}((-gov)|(-iso(b?)))?-[a-z]+-\d{{1}}:\d{{12}}:(function:[a-zA-Z0-9-_]+(:(\$LATEST|[a-zA-Z0-9-_]+))?|layer:[a-zA-Z0-9-_]+)"
4158-
)
4159-
4160-
groups = pattern_match.groupdict()
4161-
fn_name = groups.get("function_name")
4162-
4163-
if groups.get("qualifier"):
4164-
raise InvalidParameterValueException(
4165-
"Tags on function aliases and versions are not supported. Please specify a function ARN.",
4166-
Type="User",
4167-
)
4168-
4169-
account_id, region = api_utils.get_account_and_region(resource, context)
4170-
fn = self._get_function(function_name=fn_name, account_id=account_id, region=region)
4171-
4172-
self._update_tags(fn, tags)
4217+
if (resource_id := extract_resource_from_arn(resource)) and resource_id.startswith(
4218+
"function"
4219+
):
4220+
name, _, account, region = function_locators_from_arn(resource)
4221+
function = self._get_function(name, account, region)
4222+
with function.lock:
4223+
function.tags = tags
4224+
# dirty hack for changed revision id, should reevaluate model to prevent this:
4225+
latest_version = function.versions["$LATEST"]
4226+
function.versions["$LATEST"] = dataclasses.replace(
4227+
latest_version, config=dataclasses.replace(latest_version.config)
4228+
)
41734229

41744230
def list_tags(
4175-
self, context: RequestContext, resource: FunctionArn, **kwargs
4231+
self, context: RequestContext, resource: TaggableResource, **kwargs
41764232
) -> ListTagsResponse:
4177-
account_id, region = api_utils.get_account_and_region(resource, context)
4178-
function_name = api_utils.get_function_name(resource, context)
4179-
fn = self._get_function(function_name=function_name, account_id=account_id, region=region)
4180-
4181-
return ListTagsResponse(Tags=self._get_tags(fn))
4233+
tags = self._get_tags(resource)
4234+
return ListTagsResponse(Tags=tags)
41824235

41834236
def untag_resource(
4184-
self, context: RequestContext, resource: FunctionArn, tag_keys: TagKeyList, **kwargs
4237+
self, context: RequestContext, resource: TaggableResource, tag_keys: TagKeyList, **kwargs
41854238
) -> None:
41864239
if not tag_keys:
41874240
raise ValidationException(
41884241
"1 validation error detected: Value null at 'tagKeys' failed to satisfy constraint: Member must not be null"
41894242
) # should probably be generalized a bit
41904243

4191-
account_id, region = api_utils.get_account_and_region(resource, context)
4192-
function_name = api_utils.get_function_name(resource, context)
4193-
fn = self._get_function(function_name=function_name, account_id=account_id, region=region)
4244+
state = self.fetch_lambda_store_for_tagging(resource)
4245+
state.TAGS.untag_resource(resource, tag_keys)
41944246

4195-
# copy first, then set explicitly in store tags
4196-
tags = dict(fn.tags or {})
4197-
if tags:
4198-
for key in tag_keys:
4199-
if key in tags:
4200-
tags.pop(key)
4201-
self._store_tags(function=fn, tags=tags)
4247+
if (resource_id := extract_resource_from_arn(resource)) and resource_id.startswith(
4248+
"function"
4249+
):
4250+
name, _, account, region = function_locators_from_arn(resource)
4251+
function = self._get_function(name, account, region)
4252+
with function.lock:
4253+
function.tags = {
4254+
tag["Key"]: tag["Value"]
4255+
for tag in state.TAGS.list_tags_for_resource(resource).get("Tags")
4256+
}
4257+
# dirty hack for changed revision id, should reevaluate model to prevent this:
4258+
latest_version = function.versions["$LATEST"]
4259+
function.versions["$LATEST"] = dataclasses.replace(
4260+
latest_version, config=dataclasses.replace(latest_version.config)
4261+
)
42024262

42034263
# =======================================
42044264
# ======= LEGACY / DEPRECATED ========

0 commit comments

Comments
 (0)
0