diff --git a/localstack/services/cloudformation/cloudformation_starter.py b/localstack/services/cloudformation/cloudformation_starter.py index b42237b88879d..090fd39747b92 100644 --- a/localstack/services/cloudformation/cloudformation_starter.py +++ b/localstack/services/cloudformation/cloudformation_starter.py @@ -209,6 +209,9 @@ def add_default_resource_props(resource_props, stack_name, resource_name=None): if res_type == 'AWS::Lambda::EventSourceMapping' and not props.get('StartingPosition'): props['StartingPosition'] = 'LATEST' + if res_type == 'AWS::Lambda::Function' and not props.get('FunctionName'): + props['FunctionName'] = '{}-lambda-{}'.format(stack_name[:45], short_uid()) + if res_type == 'AWS::SNS::Topic' and not props.get('TopicName'): props['TopicName'] = 'topic-%s' % short_uid() @@ -248,8 +251,15 @@ def clean_json(resource_json, resources_map): LOG.info('Potential circular dependency detected when resolving Ref "%s"' % resource_json['Ref']) return resource_json['Ref'] raise - if isinstance(result, BaseModel): - if isinstance(resource_json, dict) and 'Ref' in resource_json: + if isinstance(resource_json, dict): + if isinstance(resource_json.get('Fn::GetAtt'), list) and result == resource_json: + # If the attribute cannot be resolved (i.e., result == resource_json), then return + # an empty value, to avoid returning the original JSON struct (which otherwise + # results in downstream issues, e.g., when concatenating template values). + # TODO: Note that this workaround could point towards a general issue with + # dependency resolution - in fact, this case should never be happening (but it does). + return '' + if 'Ref' in resource_json and isinstance(result, BaseModel): entity_id = get_entity_id(result, resource_json) if entity_id: return entity_id @@ -294,7 +304,7 @@ def _parse_and_create_resource(logical_id, resource_json, resources_map, region_ return None # parse and get final resource JSON - resource_tuple = parsing.parse_resource(logical_id, resource_json, resources_map) + resource_tuple = parsing.parse_resource_and_generate_name(logical_id, resource_json, resources_map) if not resource_tuple: return None _, resource_json, resource_name = resource_tuple @@ -617,7 +627,9 @@ def SQS_Queue_physical_resource_id(self): result = SQS_Queue_physical_resource_id_orig.fget(self) if '://' not in result: # convert ID to queue URL - return aws_stack.get_sqs_queue_url(result) + self._physical_resource_id = (getattr(self, '_physical_resource_id', None) or + aws_stack.get_sqs_queue_url(result)) + return self._physical_resource_id return result SQS_Queue_physical_resource_id_orig = sqs_models.Queue.physical_resource_id @@ -793,7 +805,7 @@ def Role_update_from_cloudformation_json(cls, if not hasattr(iam_models.Role, 'update_from_cloudformation_json'): iam_models.Role.update_from_cloudformation_json = Role_update_from_cloudformation_json - # patch ApiGateway Deployment + # patch ApiGateway Deployment deletion @staticmethod def depl_delete_from_cloudformation_json(resource_name, resource_json, region_name): properties = resource_json['Properties'] @@ -802,7 +814,7 @@ def depl_delete_from_cloudformation_json(resource_name, resource_json, region_na if not hasattr(apigw_models.Deployment, 'delete_from_cloudformation_json'): apigw_models.Deployment.delete_from_cloudformation_json = depl_delete_from_cloudformation_json - # patch Lambda Version + # patch Lambda Version deletion @staticmethod def vers_delete_from_cloudformation_json(resource_name, resource_json, region_name): properties = resource_json['Properties'] diff --git a/localstack/utils/bootstrap.py b/localstack/utils/bootstrap.py index aeab10e2fe149..94443cbf5a215 100644 --- a/localstack/utils/bootstrap.py +++ b/localstack/utils/bootstrap.py @@ -523,7 +523,11 @@ def run(self): LOG.warning('Thread run method %s(%s) failed: %s %s' % (self.func, self.params, e, traceback.format_exc())) finally: - self.result_future.set_result(result) + try: + self.result_future.set_result(result) + except Exception: + # this can happen as InvalidStateError on shutdown, if the task is already canceled + pass def stop(self, quiet=False): if not quiet and not self.quiet: diff --git a/localstack/utils/cloudformation/template_deployer.py b/localstack/utils/cloudformation/template_deployer.py index b386be2c8193a..83efad8377890 100644 --- a/localstack/utils/cloudformation/template_deployer.py +++ b/localstack/utils/cloudformation/template_deployer.py @@ -747,9 +747,8 @@ def retrieve_resource_details(resource_id, resource_status, resources, stack_nam resource_props = resource.get('Properties') try: if resource_type == 'Lambda::Function': - resource_props['FunctionName'] = (resource_props.get('FunctionName') or - '{}-lambda-{}'.format(stack_name[:45], common.short_uid())) - resource_id = resource_props['FunctionName'] if resource else resource_id + func_name = resolve_refs_recursively(stack_name, resource_props['FunctionName'], resources) + resource_id = func_name if resource else resource_id return aws_stack.connect_to_service('lambda').get_function(FunctionName=resource_id) elif resource_type == 'Lambda::Version': name = resource_props.get('FunctionName') diff --git a/tests/integration/serverless/package.json b/tests/integration/serverless/package.json index 15adfa44bd352..87737f17456dd 100644 --- a/tests/integration/serverless/package.json +++ b/tests/integration/serverless/package.json @@ -7,6 +7,7 @@ }, "devDependencies": { "serverless": "^1.67.1", - "serverless-localstack": "^0.4.24" + "serverless-localstack": "^0.4.24", + "serverless-deployment-bucket": "^1.1.2" } } diff --git a/tests/integration/serverless/serverless.yml b/tests/integration/serverless/serverless.yml index da73e5282ba38..60de4ea6955e7 100644 --- a/tests/integration/serverless/serverless.yml +++ b/tests/integration/serverless/serverless.yml @@ -7,6 +7,8 @@ provider: versionFunctions: false timeout: 900 runtime: "nodejs12.x" + deploymentBucket: + name: custom-sls-depl-bucket-123 functions: test: @@ -25,8 +27,10 @@ functions: path: /test/v1 method: get integration: lambda-proxy + plugins: - - "serverless-localstack" + - serverless-deployment-bucket + - serverless-localstack custom: localstack: diff --git a/tests/integration/test_cloudformation.py b/tests/integration/test_cloudformation.py index f927c31312bb5..08b76f5b429fc 100644 --- a/tests/integration/test_cloudformation.py +++ b/tests/integration/test_cloudformation.py @@ -1147,6 +1147,8 @@ def test_deploy_stack_with_iam_role(self): role_name = 'role-%s' % short_uid() cloudformation = aws_stack.connect_to_service('cloudformation') + iam_client = aws_stack.connect_to_service('iam') + roles_before = iam_client.list_roles()['Roles'] try: cloudformation.describe_stacks( @@ -1188,11 +1190,10 @@ def test_deploy_stack_with_iam_role(self): stack = rs['Stacks'][0] self.assertEqual(stack['StackName'], stack_name) - iam_client = aws_stack.connect_to_service('iam') rs = iam_client.list_roles() - self.assertEqual(len(rs['Roles']), 1) - self.assertEqual(rs['Roles'][0]['RoleName'], role_name) + self.assertEqual(len(rs['Roles']), len(roles_before) + 1) + self.assertEqual(rs['Roles'][-1]['RoleName'], role_name) rs = iam_client.list_role_policies( RoleName=role_name