From 983b0152f4d9144a0094d733ce2d1832be143077 Mon Sep 17 00:00:00 2001 From: Joseph Torcasso <87090265+jatorcasso@users.noreply.github.com> Date: Wed, 1 Jun 2022 01:20:57 -0400 Subject: [PATCH] lambda_info - refactor to fix bug when querying all lambdas (#1152) lambda_info - refactor to fix bug when querying all lambdas Depends-On: ansible/ansible-zuul-jobs#1558 SUMMARY Fix bug that forces query: config when getting info for all lambdas. Refactored to return the expected info Add extra cleanup at end of tests Fixes #1151 ISSUE TYPE Bugfix Pull Request COMPONENT NAME lambda_info ADDITIONAL INFORMATION This module also currently returns a dict of dicts (as opposed to a list of dicts), but I wanted to keep the scope of this PR to fixing the bug. Reviewed-by: Mark Chappell Reviewed-by: Joseph Torcasso Reviewed-by: Jill R This commit was initially merged in https://github.com/ansible-collections/community.aws See: https://github.com/ansible-collections/community.aws/commit/0c76aedd4700a85ad6ee6971ffb787f2a2df1c2b --- plugins/modules/lambda_info.py | 226 +++++++++--------- .../integration/targets/lambda/tasks/main.yml | 87 ++++++- 2 files changed, 192 insertions(+), 121 deletions(-) diff --git a/plugins/modules/lambda_info.py b/plugins/modules/lambda_info.py index c76ecba3d1e..1ad2749c5f8 100644 --- a/plugins/modules/lambda_info.py +++ b/plugins/modules/lambda_info.py @@ -13,16 +13,17 @@ short_description: Gathers AWS Lambda function details description: - Gathers various details related to Lambda functions, including aliases, versions and event source mappings. - - Use module M(community.aws.lambda) to manage the lambda function itself, M(community.aws.lambda_alias) to manage function aliases and - M(community.aws.lambda_event) to manage lambda event source mappings. + - Use module M(community.aws.lambda) to manage the lambda function itself, M(community.aws.lambda_alias) to manage function aliases, + M(community.aws.lambda_event) to manage lambda event source mappings, and M(community.aws.lambda_policy) to manage policy statements. options: query: description: - - Specifies the resource type for which to gather information. Leave blank to retrieve all information. + - Specifies the resource type for which to gather information. + - Defaults to C(all) when I(function_name) is specified. + - Defaults to C(config) when I(function_name) is NOT specified. choices: [ "aliases", "all", "config", "mappings", "policy", "versions", "tags" ] - default: "all" type: str function_name: description: @@ -48,17 +49,20 @@ query: all function_name: myFunction register: my_function_details + # List all versions of a function - name: List function versions community.aws.lambda_info: query: versions function_name: myFunction register: my_function_versions -# List all lambda function versions -- name: List all function + +# List all info for all functions +- name: List all functions community.aws.lambda_info: query: all register: output + - name: show Lambda information ansible.builtin.debug: msg: "{{ output['function'] }}" @@ -120,108 +124,118 @@ def fix_return(node): return node_value -def alias_details(client, module): +def alias_details(client, module, function_name): """ Returns list of aliases for a specified function. :param client: AWS API client reference (boto3) :param module: Ansible module reference + :param function_name (str): Name of Lambda function to query :return dict: """ lambda_info = dict() - function_name = module.params.get('function_name') - if function_name: - try: - lambda_info.update(aliases=_paginate(client, 'list_aliases', FunctionName=function_name)['Aliases']) - except is_boto3_error_code('ResourceNotFoundException'): - lambda_info.update(aliases=[]) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Trying to get aliases") - else: - module.fail_json(msg='Parameter function_name required for query=aliases.') + try: + lambda_info.update(aliases=_paginate(client, 'list_aliases', FunctionName=function_name)['Aliases']) + except is_boto3_error_code('ResourceNotFoundException'): + lambda_info.update(aliases=[]) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="Trying to get aliases") - return {function_name: camel_dict_to_snake_dict(lambda_info)} + return camel_dict_to_snake_dict(lambda_info) -def all_details(client, module): +def list_lambdas(client, module): """ - Returns all lambda related facts. + Returns queried facts for a specified function (or all functions). :param client: AWS API client reference (boto3) :param module: Ansible module reference :return dict: """ - lambda_info = dict() - function_name = module.params.get('function_name') if function_name: - lambda_info[function_name] = {} - lambda_info[function_name].update(config_details(client, module)[function_name]) - lambda_info[function_name].update(alias_details(client, module)[function_name]) - lambda_info[function_name].update(policy_details(client, module)[function_name]) - lambda_info[function_name].update(version_details(client, module)[function_name]) - lambda_info[function_name].update(mapping_details(client, module)[function_name]) - lambda_info[function_name].update(tags_details(client, module)[function_name]) + # Function name is specified - retrieve info on that function + function_names = [function_name] + else: - lambda_info.update(config_details(client, module)) + # Function name is not specified - retrieve all function names + all_function_info = _paginate(client, 'list_functions')['Functions'] + function_names = [function_info['FunctionName'] for function_info in all_function_info] + + query = module.params['query'] + lambdas = dict() + + for function_name in function_names: + lambdas[function_name] = {} - return lambda_info + if query == 'all': + lambdas[function_name].update(config_details(client, module, function_name)) + lambdas[function_name].update(alias_details(client, module, function_name)) + lambdas[function_name].update(policy_details(client, module, function_name)) + lambdas[function_name].update(version_details(client, module, function_name)) + lambdas[function_name].update(mapping_details(client, module, function_name)) + lambdas[function_name].update(tags_details(client, module, function_name)) + elif query == 'config': + lambdas[function_name].update(config_details(client, module, function_name)) -def config_details(client, module): + elif query == 'aliases': + lambdas[function_name].update(alias_details(client, module, function_name)) + + elif query == 'policy': + lambdas[function_name].update(policy_details(client, module, function_name)) + + elif query == 'versions': + lambdas[function_name].update(version_details(client, module, function_name)) + + elif query == 'mappings': + lambdas[function_name].update(mapping_details(client, module, function_name)) + + elif query == 'tags': + lambdas[function_name].update(tags_details(client, module, function_name)) + + return lambdas + + +def config_details(client, module, function_name): """ - Returns configuration details for one or all lambda functions. + Returns configuration details for a lambda function. :param client: AWS API client reference (boto3) :param module: Ansible module reference + :param function_name (str): Name of Lambda function to query :return dict: """ lambda_info = dict() - function_name = module.params.get('function_name') - if function_name: - try: - lambda_info.update(client.get_function_configuration(aws_retry=True, FunctionName=function_name)) - except is_boto3_error_code('ResourceNotFoundException'): - lambda_info.update(function={}) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Trying to get {0} configuration".format(function_name)) - else: - try: - lambda_info.update(function_list=_paginate(client, 'list_functions')['Functions']) - except is_boto3_error_code('ResourceNotFoundException'): - lambda_info.update(function_list=[]) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Trying to get function list") - - functions = dict() - for func in lambda_info.pop('function_list', []): - func['tags'] = client.get_function(FunctionName=func['FunctionName']).get('Tags', {}) - functions[func['FunctionName']] = camel_dict_to_snake_dict(func) - return functions + try: + lambda_info.update(client.get_function_configuration(aws_retry=True, FunctionName=function_name)) + except is_boto3_error_code('ResourceNotFoundException'): + lambda_info.update(function={}) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="Trying to get {0} configuration".format(function_name)) - return {function_name: camel_dict_to_snake_dict(lambda_info)} + return camel_dict_to_snake_dict(lambda_info) -def mapping_details(client, module): +def mapping_details(client, module, function_name): """ Returns all lambda event source mappings. :param client: AWS API client reference (boto3) :param module: Ansible module reference + :param function_name (str): Name of Lambda function to query :return dict: """ lambda_info = dict() params = dict() - function_name = module.params.get('function_name') - if function_name: - params['FunctionName'] = module.params.get('function_name') + params['FunctionName'] = function_name if module.params.get('event_source_arn'): params['EventSourceArn'] = module.params.get('event_source_arn') @@ -233,86 +247,74 @@ def mapping_details(client, module): except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except module.fail_json_aws(e, msg="Trying to get source event mappings") - if function_name: - return {function_name: camel_dict_to_snake_dict(lambda_info)} - return camel_dict_to_snake_dict(lambda_info) -def policy_details(client, module): +def policy_details(client, module, function_name): """ Returns policy attached to a lambda function. :param client: AWS API client reference (boto3) :param module: Ansible module reference + :param function_name (str): Name of Lambda function to query :return dict: """ lambda_info = dict() - function_name = module.params.get('function_name') - if function_name: - try: - # get_policy returns a JSON string so must convert to dict before reassigning to its key - lambda_info.update(policy=json.loads(client.get_policy(aws_retry=True, FunctionName=function_name)['Policy'])) - except is_boto3_error_code('ResourceNotFoundException'): - lambda_info.update(policy={}) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Trying to get {0} policy".format(function_name)) - else: - module.fail_json(msg='Parameter function_name required for query=policy.') + try: + # get_policy returns a JSON string so must convert to dict before reassigning to its key + lambda_info.update(policy=json.loads(client.get_policy(aws_retry=True, FunctionName=function_name)['Policy'])) + except is_boto3_error_code('ResourceNotFoundException'): + lambda_info.update(policy={}) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="Trying to get {0} policy".format(function_name)) - return {function_name: camel_dict_to_snake_dict(lambda_info)} + return camel_dict_to_snake_dict(lambda_info) -def version_details(client, module): +def version_details(client, module, function_name): """ Returns all lambda function versions. :param client: AWS API client reference (boto3) :param module: Ansible module reference + :param function_name (str): Name of Lambda function to query :return dict: """ lambda_info = dict() - function_name = module.params.get('function_name') - if function_name: - try: - lambda_info.update(versions=_paginate(client, 'list_versions_by_function', FunctionName=function_name)['Versions']) - except is_boto3_error_code('ResourceNotFoundException'): - lambda_info.update(versions=[]) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Trying to get {0} versions".format(function_name)) - else: - module.fail_json(msg='Parameter function_name required for query=versions.') + try: + lambda_info.update(versions=_paginate(client, 'list_versions_by_function', FunctionName=function_name)['Versions']) + except is_boto3_error_code('ResourceNotFoundException'): + lambda_info.update(versions=[]) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="Trying to get {0} versions".format(function_name)) - return {function_name: camel_dict_to_snake_dict(lambda_info)} + return camel_dict_to_snake_dict(lambda_info) -def tags_details(client, module): +def tags_details(client, module, function_name): """ - Returns tag details for one or all lambda functions. + Returns tag details for a lambda function. :param client: AWS API client reference (boto3) :param module: Ansible module reference + :param function_name (str): Name of Lambda function to query :return dict: """ lambda_info = dict() - function_name = module.params.get('function_name') - if function_name: - try: - lambda_info.update(tags=client.get_function(aws_retry=True, FunctionName=function_name).get('Tags', {})) - except is_boto3_error_code('ResourceNotFoundException'): - lambda_info.update(function={}) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Trying to get {0} tags".format(function_name)) - else: - module.fail_json(msg='Parameter function_name required for query=tags.') + try: + lambda_info.update(tags=client.get_function(aws_retry=True, FunctionName=function_name).get('Tags', {})) + except is_boto3_error_code('ResourceNotFoundException'): + lambda_info.update(function={}) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="Trying to get {0} tags".format(function_name)) - return {function_name: camel_dict_to_snake_dict(lambda_info)} + return camel_dict_to_snake_dict(lambda_info) def main(): @@ -323,7 +325,7 @@ def main(): """ argument_spec = dict( function_name=dict(required=False, default=None, aliases=['function', 'name']), - query=dict(required=False, choices=['aliases', 'all', 'config', 'mappings', 'policy', 'versions', 'tags'], default='all'), + query=dict(required=False, choices=['aliases', 'all', 'config', 'mappings', 'policy', 'versions', 'tags'], default=None), event_source_arn=dict(required=False, default=None), ) @@ -344,20 +346,18 @@ def main(): if len(function_name) > 64: module.fail_json(msg='Function name "{0}" exceeds 64 character limit'.format(function_name)) - client = module.client('lambda', retry_decorator=AWSRetry.jittered_backoff()) + # create default values for query if not specified. + # if function name exists, query should default to 'all'. + # if function name does not exist, query should default to 'config' to limit the runtime when listing all lambdas. + if not module.params.get('query'): + if function_name: + module.params['query'] = 'all' + else: + module.params['query'] = 'config' - invocations = dict( - aliases='alias_details', - all='all_details', - config='config_details', - mappings='mapping_details', - policy='policy_details', - versions='version_details', - tags='tags_details', - ) + client = module.client('lambda', retry_decorator=AWSRetry.jittered_backoff()) - this_module_function = globals()[invocations[module.params['query']]] - all_facts = fix_return(this_module_function(client, module)) + all_facts = fix_return(list_lambdas(client, module)) results = dict(function=all_facts, changed=False) diff --git a/tests/integration/targets/lambda/tasks/main.yml b/tests/integration/targets/lambda/tasks/main.yml index 65e1f9bc858..6ce1d7bae14 100644 --- a/tests/integration/targets/lambda/tasks/main.yml +++ b/tests/integration/targets/lambda/tasks/main.yml @@ -252,10 +252,9 @@ - result.configuration.runtime == 'python3.6' - result.configuration.tracing_config.mode == 'PassThrough' - # Query the Lambda - - name: lambda_info | Gather all infos for given lambda function + # Test lambda_info + - name: lambda_info | Gather all infos for all lambda functions lambda_info: - name: '{{ lambda_function_name }}' query: all register: lambda_infos_all check_mode: yes @@ -263,17 +262,54 @@ assert: that: - lambda_infos_all is not failed + - lambda_infos_all.function | length > 0 - lambda_infos_all.function[lambda_function_name].function_name == lambda_function_name - lambda_infos_all.function[lambda_function_name].runtime == "python3.6" + - lambda_infos_all.function[lambda_function_name].description == "" + - lambda_infos_all.function[lambda_function_name].function_arn is defined + - lambda_infos_all.function[lambda_function_name].handler == "mini_lambda.handler" - lambda_infos_all.function[lambda_function_name].versions is defined - lambda_infos_all.function[lambda_function_name].aliases is defined - lambda_infos_all.function[lambda_function_name].policy is defined - lambda_infos_all.function[lambda_function_name].mappings is defined - - lambda_infos_all.function[lambda_function_name].description == "" - - lambda_infos_all.function[lambda_function_name].function_arn is defined - - lambda_infos_all.function[lambda_function_name].handler == "mini_lambda.handler" - lambda_infos_all.function[lambda_function_name].tags is defined + - name: lambda_info | Ensure default query value is 'config' when function name omitted + lambda_info: + register: lambda_infos_query_config + check_mode: yes + - name: lambda_info | Assert successfull retrieval of all information + assert: + that: + - lambda_infos_query_config is not failed + - lambda_infos_query_config.function | length > 0 + - lambda_infos_query_config.function[lambda_function_name].function_name == lambda_function_name + - lambda_infos_query_config.function[lambda_function_name].runtime == "python3.6" + - lambda_infos_query_config.function[lambda_function_name].description == "" + - lambda_infos_query_config.function[lambda_function_name].function_arn is defined + - lambda_infos_query_config.function[lambda_function_name].handler == "mini_lambda.handler" + - lambda_infos_query_config.function[lambda_function_name].versions is not defined + - lambda_infos_query_config.function[lambda_function_name].aliases is not defined + - lambda_infos_query_config.function[lambda_function_name].policy is not defined + - lambda_infos_query_config.function[lambda_function_name].mappings is not defined + - lambda_infos_query_config.function[lambda_function_name].tags is not defined + + - name: lambda_info | Ensure default query value is 'all' when function name specified + lambda_info: + name: '{{ lambda_function_name }}' + register: lambda_infos_query_all + - name: lambda_info | Assert successfull retrieval of all information + assert: + that: + - lambda_infos_query_all is not failed + - lambda_infos_query_all.function | length == 1 + - lambda_infos_query_all.function[lambda_function_name].versions|length > 0 + - lambda_infos_query_all.function[lambda_function_name].function_name is defined + - lambda_infos_query_all.function[lambda_function_name].policy is defined + - lambda_infos_query_all.function[lambda_function_name].aliases is defined + - lambda_infos_query_all.function[lambda_function_name].mappings is defined + - lambda_infos_query_all.function[lambda_function_name].tags is defined + - name: lambda_info | Gather version infos for given lambda function lambda_info: name: '{{ lambda_function_name }}' @@ -283,8 +319,13 @@ assert: that: - lambda_infos_versions is not failed + - lambda_infos_versions.function | length == 1 - lambda_infos_versions.function[lambda_function_name].versions|length > 0 - lambda_infos_versions.function[lambda_function_name].function_name is undefined + - lambda_infos_versions.function[lambda_function_name].policy is undefined + - lambda_infos_versions.function[lambda_function_name].aliases is undefined + - lambda_infos_versions.function[lambda_function_name].mappings is undefined + - lambda_infos_versions.function[lambda_function_name].tags is undefined - name: lambda_info | Gather config infos for given lambda function lambda_info: @@ -295,9 +336,14 @@ assert: that: - lambda_infos_config is not failed + - lambda_infos_config.function | length == 1 - lambda_infos_config.function[lambda_function_name].function_name == lambda_function_name - lambda_infos_config.function[lambda_function_name].description is defined - lambda_infos_config.function[lambda_function_name].versions is undefined + - lambda_infos_config.function[lambda_function_name].policy is undefined + - lambda_infos_config.function[lambda_function_name].aliases is undefined + - lambda_infos_config.function[lambda_function_name].mappings is undefined + - lambda_infos_config.function[lambda_function_name].tags is undefined - name: lambda_info | Gather policy infos for given lambda function lambda_info: @@ -308,8 +354,13 @@ assert: that: - lambda_infos_policy is not failed + - lambda_infos_policy.function | length == 1 - lambda_infos_policy.function[lambda_function_name].policy is defined - lambda_infos_policy.function[lambda_function_name].versions is undefined + - lambda_infos_policy.function[lambda_function_name].function_name is undefined + - lambda_infos_policy.function[lambda_function_name].aliases is undefined + - lambda_infos_policy.function[lambda_function_name].mappings is undefined + - lambda_infos_policy.function[lambda_function_name].tags is undefined - name: lambda_info | Gather aliases infos for given lambda function lambda_info: @@ -320,7 +371,13 @@ assert: that: - lambda_infos_aliases is not failed + - lambda_infos_aliases.function | length == 1 - lambda_infos_aliases.function[lambda_function_name].aliases is defined + - lambda_infos_aliases.function[lambda_function_name].versions is undefined + - lambda_infos_aliases.function[lambda_function_name].function_name is undefined + - lambda_infos_aliases.function[lambda_function_name].policy is undefined + - lambda_infos_aliases.function[lambda_function_name].mappings is undefined + - lambda_infos_aliases.function[lambda_function_name].tags is undefined - name: lambda_info | Gather mappings infos for given lambda function lambda_info: @@ -331,7 +388,13 @@ assert: that: - lambda_infos_mappings is not failed + - lambda_infos_mappings.function | length == 1 - lambda_infos_mappings.function[lambda_function_name].mappings is defined + - lambda_infos_mappings.function[lambda_function_name].versions is undefined + - lambda_infos_mappings.function[lambda_function_name].function_name is undefined + - lambda_infos_mappings.function[lambda_function_name].aliases is undefined + - lambda_infos_mappings.function[lambda_function_name].policy is undefined + - lambda_infos_mappings.function[lambda_function_name].tags is undefined # More Lambda update tests - name: test state=present with all nullable variables explicitly set to null @@ -523,11 +586,19 @@ - result is not failed always: - - name: ensure function is absent at end of test + + - name: ensure functions are absent at end of test lambda: - name: '{{lambda_function_name}}' + name: "{{ item }}" state: absent ignore_errors: true + with_items: + - "{{ lambda_function_name }}" + - "{{ lambda_function_name }}_1" + - "{{ lambda_function_name }}_2" + - "{{ lambda_function_name }}_3" + - "{{ lambda_function_name }}_4" + - name: ensure role has been removed at end of test iam_role: name: '{{ lambda_role_name }}'