diff --git a/changelogs/fragments/1307-botocore-configs.yml b/changelogs/fragments/1307-botocore-configs.yml new file mode 100644 index 00000000000..3a2481f9f58 --- /dev/null +++ b/changelogs/fragments/1307-botocore-configs.yml @@ -0,0 +1,2 @@ +minor_changes: +- module_utils/botocore - added support to ``_boto3_conn`` for passing dictionaries of configuration (https://github.com/ansible-collections/amazon.aws/pull/1307). diff --git a/plugins/module_utils/botocore.py b/plugins/module_utils/botocore.py index d9c418ff025..1da09d44c7c 100644 --- a/plugins/module_utils/botocore.py +++ b/plugins/module_utils/botocore.py @@ -79,6 +79,18 @@ def boto3_conn(module, conn_type=None, resource=None, region=None, endpoint=None "environment variables or module parameters" % module._name) +def _merge_botocore_config(config_a, config_b): + """ + Merges the extra configuration options from config_b into config_a. + Supports both botocore.config.Config objects and dicts + """ + if not config_b: + return config_a + if not isinstance(config_b, botocore.config.Config): + config_b = botocore.config.Config(**config_b) + return config_a.merge(config_b) + + def _boto3_conn(conn_type=None, resource=None, region=None, endpoint=None, **params): """ Builds a boto3 resource/client connection cleanly wrapping the most common failures. @@ -96,10 +108,8 @@ def _boto3_conn(conn_type=None, resource=None, region=None, endpoint=None, **par user_agent_extra='Ansible/{0}'.format(__version__), ) - if params.get('config') is not None: - config = config.merge(params.pop('config')) - if params.get('aws_config') is not None: - config = config.merge(params.pop('aws_config')) + for param in ("config", "aws_config"): + config = _merge_botocore_config(config, params.pop(param, None)) session = boto3.session.Session( profile_name=profile, diff --git a/tests/unit/module_utils/botocore/test_merge_botocore_config.py b/tests/unit/module_utils/botocore/test_merge_botocore_config.py new file mode 100644 index 00000000000..2adefd7835c --- /dev/null +++ b/tests/unit/module_utils/botocore/test_merge_botocore_config.py @@ -0,0 +1,71 @@ +# (c) 2022 Red Hat Inc. +# +# This file is part of Ansible +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +import pprint +import pytest +from unittest.mock import MagicMock +from unittest.mock import sentinel +from unittest.mock import call + +try: + import botocore +except ImportError: + # Handled by HAS_BOTO3 + pass + +import ansible_collections.amazon.aws.plugins.module_utils.botocore as utils_botocore +from ansible_collections.amazon.aws.plugins.module_utils.exceptions import AnsibleBotocoreError + +MINIMAL_CONFIG = { + "user_agent_extra": "Ansible/unit-test", +} + + +@pytest.fixture +def basic_config(): + config = botocore.config.Config(**MINIMAL_CONFIG) + return config + + +def test_none_config(monkeypatch, basic_config): + original_options = basic_config._user_provided_options.copy() + + monkeypatch.setattr(basic_config, "merge", MagicMock(name="merge")) + updated_config = utils_botocore._merge_botocore_config(basic_config, None) + assert not basic_config.merge.called + assert basic_config._user_provided_options == original_options + assert updated_config._user_provided_options == original_options + + +def test_botocore_config(basic_config): + original_options = basic_config._user_provided_options.copy() + config_b = botocore.config.Config(parameter_validation=False) + updated_config = utils_botocore._merge_botocore_config(basic_config, config_b) + + assert basic_config._user_provided_options == original_options + assert not updated_config._user_provided_options == original_options + assert updated_config._user_provided_options.get("parameter_validation") is False + assert updated_config._user_provided_options.get("user_agent_extra") == "Ansible/unit-test" + + config_c = botocore.config.Config(user_agent_extra="Ansible/unit-test Updated") + updated_config = utils_botocore._merge_botocore_config(updated_config, config_c) + assert updated_config._user_provided_options.get("parameter_validation") is False + assert updated_config._user_provided_options.get("user_agent_extra") == "Ansible/unit-test Updated" + + +def test_botocore_dict(basic_config): + original_options = basic_config._user_provided_options.copy() + config_b = dict(parameter_validation=False) + updated_config = utils_botocore._merge_botocore_config(basic_config, config_b) + + assert basic_config._user_provided_options == original_options + assert not updated_config._user_provided_options == original_options + assert updated_config._user_provided_options.get("parameter_validation") is False + assert updated_config._user_provided_options.get("user_agent_extra") == "Ansible/unit-test" + + config_c = dict(user_agent_extra="Ansible/unit-test Updated") + updated_config = utils_botocore._merge_botocore_config(updated_config, config_c) + assert updated_config._user_provided_options.get("parameter_validation") is False + assert updated_config._user_provided_options.get("user_agent_extra") == "Ansible/unit-test Updated"