|
41 | 41 | Description,
|
42 | 42 | DestinationConfig,
|
43 | 43 | EventSourceMappingConfiguration,
|
44 |
| - FunctionArn, |
45 | 44 | FunctionCodeLocation,
|
46 | 45 | FunctionConfiguration,
|
47 | 46 | FunctionEventInvokeConfig,
|
|
127 | 126 | StatementId,
|
128 | 127 | StateReasonCode,
|
129 | 128 | String,
|
| 129 | + TaggableResource, |
130 | 130 | TagKeyList,
|
131 | 131 | Tags,
|
132 | 132 | TracingMode,
|
|
221 | 221 | from localstack.services.plugins import ServiceLifecycleHook
|
222 | 222 | from localstack.state import StateVisitor
|
223 | 223 | from localstack.utils.aws.arns import (
|
| 224 | + ArnData, |
| 225 | + extract_resource_from_arn, |
224 | 226 | extract_service_from_arn,
|
225 | 227 | get_partition,
|
| 228 | + lambda_event_source_mapping_arn, |
| 229 | + parse_arn, |
226 | 230 | )
|
227 | 231 | from localstack.utils.bootstrap import is_api_enabled
|
228 | 232 | from localstack.utils.collections import PaginatedList
|
@@ -394,6 +398,18 @@ def _get_function(function_name: str, account_id: str, region: str) -> Function:
|
394 | 398 | )
|
395 | 399 | return function
|
396 | 400 |
|
| 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 | + |
397 | 413 | @staticmethod
|
398 | 414 | def _validate_qualifier_expression(qualifier: str) -> None:
|
399 | 415 | if error_messages := api_utils.validate_qualifier(qualifier):
|
@@ -988,7 +1004,7 @@ def create_function(
|
988 | 1004 | )
|
989 | 1005 | fn.versions["$LATEST"] = version
|
990 | 1006 | if request.get("Tags"):
|
991 |
| - self._store_tags(fn, request["Tags"]) |
| 1007 | + self._store_tags(arn.unqualified_arn(), request["Tags"]) |
992 | 1008 | # TODO: should validation failures here "fail" the function creation like it is now?
|
993 | 1009 | state.functions[function_name] = fn
|
994 | 1010 | self.lambda_service.create_function_version(version)
|
@@ -1453,7 +1469,7 @@ def get_function(
|
1453 | 1469 | account_id=account_id,
|
1454 | 1470 | region=region,
|
1455 | 1471 | )
|
1456 |
| - tags = self._get_tags(fn) |
| 1472 | + tags = self._get_tags(api_utils.unqualified_lambda_arn(function_name, account_id, region)) |
1457 | 1473 | additional_fields = {}
|
1458 | 1474 | if tags:
|
1459 | 1475 | additional_fields["Tags"] = tags
|
@@ -1873,6 +1889,8 @@ def create_event_source_mapping_v2(
|
1873 | 1889 | esm_worker = EsmWorkerFactory(esm_config, function_role, enabled).get_esm_worker()
|
1874 | 1890 | self.esm_workers[esm_worker.uuid] = esm_worker
|
1875 | 1891 | # TODO: check StateTransitionReason, LastModified, LastProcessingResult (concurrent updates requires locking!)
|
| 1892 | + if tags := request.get("Tags"): |
| 1893 | + self._store_tags(esm_config.get("EventSourceMappingArn"), tags) |
1876 | 1894 | esm_worker.create()
|
1877 | 1895 | return esm_config
|
1878 | 1896 |
|
@@ -4118,87 +4136,129 @@ def delete_function_concurrency(
|
4118 | 4136 | # =======================================
|
4119 | 4137 | # =============== TAGS ===============
|
4120 | 4138 | # =======================================
|
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 |
4122 | 4140 |
|
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 |
4125 | 4148 |
|
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: |
4128 | 4152 | raise InvalidParameterValueException(
|
4129 | 4153 | "Number of tags exceeds resource tag limit.", Type="User"
|
4130 | 4154 | )
|
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}" |
4137 | 4163 | )
|
4138 | 4164 |
|
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] |
4144 | 4207 |
|
4145 | 4208 | def tag_resource(
|
4146 |
| - self, context: RequestContext, resource: FunctionArn, tags: Tags, **kwargs |
| 4209 | + self, context: RequestContext, resource: TaggableResource, tags: Tags, **kwargs |
4147 | 4210 | ) -> None:
|
4148 | 4211 | if not tags:
|
4149 | 4212 | raise InvalidParameterValueException(
|
4150 | 4213 | "An error occurred and the request cannot be processed.", Type="User"
|
4151 | 4214 | )
|
| 4215 | + self._store_tags(resource, tags) |
4152 | 4216 |
|
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 | + ) |
4173 | 4229 |
|
4174 | 4230 | def list_tags(
|
4175 |
| - self, context: RequestContext, resource: FunctionArn, **kwargs |
| 4231 | + self, context: RequestContext, resource: TaggableResource, **kwargs |
4176 | 4232 | ) -> 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) |
4182 | 4235 |
|
4183 | 4236 | def untag_resource(
|
4184 |
| - self, context: RequestContext, resource: FunctionArn, tag_keys: TagKeyList, **kwargs |
| 4237 | + self, context: RequestContext, resource: TaggableResource, tag_keys: TagKeyList, **kwargs |
4185 | 4238 | ) -> None:
|
4186 | 4239 | if not tag_keys:
|
4187 | 4240 | raise ValidationException(
|
4188 | 4241 | "1 validation error detected: Value null at 'tagKeys' failed to satisfy constraint: Member must not be null"
|
4189 | 4242 | ) # should probably be generalized a bit
|
4190 | 4243 |
|
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) |
4194 | 4246 |
|
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 | + ) |
4202 | 4262 |
|
4203 | 4263 | # =======================================
|
4204 | 4264 | # ======= LEGACY / DEPRECATED ========
|
|
0 commit comments