diff --git a/changelogs/fragments/122-aws_secret-add-on_missing-and-on_denied-option.yml b/changelogs/fragments/122-aws_secret-add-on_missing-and-on_denied-option.yml new file mode 100644 index 00000000000..67a6dddbb4a --- /dev/null +++ b/changelogs/fragments/122-aws_secret-add-on_missing-and-on_denied-option.yml @@ -0,0 +1,2 @@ +minor_changes: + - aws_secret - add "on_missing" and "on_denied" option (https://github.com/ansible-collections/amazon.aws/pull/122). diff --git a/plugins/lookup/aws_secret.py b/plugins/lookup/aws_secret.py index 8de2d8103b5..f8b978fd23f 100644 --- a/plugins/lookup/aws_secret.py +++ b/plugins/lookup/aws_secret.py @@ -37,6 +37,24 @@ - This is useful for overcoming the 4096 character limit imposed by AWS. type: boolean default: false + on_missing: + description: + - Action to take if the secret is missing. + - C(error) will raise a fatal error when the secret is missing. + - C(skip) will silently ignore the missing secret. + - C(warn) will skip over the missing secret but issue a warning. + default: error + type: string + choices: ['error', 'skip', 'warn'] + on_denied: + description: + - Action to take if access to the secret is denied. + - C(error) will raise a fatal error when access to the secret is denied. + - C(skip) will silently ignore the denied secret. + - C(warn) will skip over the denied secret but issue a warning. + default: error + type: string + choices: ['error', 'skip', 'warn'] ''' EXAMPLES = r""" @@ -51,6 +69,12 @@ password: "{{ lookup('aws_secret', 'DbSecret') }}" tags: Environment: staging + + - name: skip if secret does not exist + debug: msg="{{ lookup('aws_secret', 'secret-not-exist', on_missing='skip')}}" + + - name: warn if access to the secret is denied + debug: msg="{{ lookup('aws_secret', 'secret-denied', on_denied='warn')}}" """ RETURN = r""" @@ -60,6 +84,7 @@ """ from ansible.errors import AnsibleError +from ansible.module_utils.six import string_types try: import boto3 @@ -70,6 +95,7 @@ from ansible.plugins import AnsiblePlugin from ansible.plugins.lookup import LookupBase from ansible.module_utils._text import to_native +from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code def _boto3_conn(region, credentials): @@ -108,6 +134,14 @@ def _get_credentials(self): def run(self, terms, variables, **kwargs): + missing = kwargs.get('on_missing', 'error').lower() + if not isinstance(missing, string_types) or missing not in ['error', 'warn', 'skip']: + raise AnsibleError('"on_missing" must be a string and one of "error", "warn" or "skip", not %s' % missing) + + denied = kwargs.get('on_denied', 'error').lower() + if not isinstance(denied, string_types) or denied not in ['error', 'warn', 'skip']: + raise AnsibleError('"on_denied" must be a string and one of "error", "warn" or "skip", not %s' % denied) + self.set_options(var_options=variables, direct=kwargs) boto_credentials = self._get_credentials() @@ -129,7 +163,17 @@ def run(self, terms, variables, **kwargs): secrets.append(response['SecretBinary']) if 'SecretString' in response: secrets.append(response['SecretString']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + except is_boto3_error_code('ResourceNotFoundException'): + if missing == 'error': + raise AnsibleError("Failed to find secret %s (ResourceNotFound)" % term) + elif missing == 'warn': + self._display.warning('Skipping, did not find secret %s' % term) + except is_boto3_error_code('AccessDeniedException'): # pylint: disable=duplicate-except + if denied == 'error': + raise AnsibleError("Failed to access secret %s (AccessDenied)" % term) + elif denied == 'warn': + self._display.warning('Skipping, access denied for secret %s' % term) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except raise AnsibleError("Failed to retrieve secret: %s" % to_native(e)) if kwargs.get('join'): diff --git a/tests/unit/plugins/lookup/test_aws_secret.py b/tests/unit/plugins/lookup/test_aws_secret.py index aa5b02d5520..1f3957419ee 100644 --- a/tests/unit/plugins/lookup/test_aws_secret.py +++ b/tests/unit/plugins/lookup/test_aws_secret.py @@ -21,9 +21,10 @@ import pytest import datetime +import sys +from copy import copy from ansible.errors import AnsibleError - from ansible.plugins.loader import lookup_loader try: @@ -77,14 +78,48 @@ def test_lookup_variable(mocker, dummy_credentials): aws_secret_access_key="notasecret", aws_session_token=None) -error_response = {'Error': {'Code': 'ResourceNotFoundException', 'Message': 'Fake Testing Error'}} +error_response_missing = {'Error': {'Code': 'ResourceNotFoundException', 'Message': 'Fake Not Found Error'}} +error_response_denied = {'Error': {'Code': 'AccessDeniedException', 'Message': 'Fake Denied Error'}} operation_name = 'FakeOperation' -def test_warn_denied_variable(mocker, dummy_credentials): +def test_on_missing_option(mocker, dummy_credentials): + boto3_double = mocker.MagicMock() + boto3_double.Session.return_value.client.return_value.get_secret_value.side_effect = ClientError(error_response_missing, operation_name) + + with pytest.raises(AnsibleError, match="ResourceNotFound"): + mocker.patch.object(boto3, 'session', boto3_double) + lookup_loader.get('amazon.aws.aws_secret').run(["missing_secret"], None, **dummy_credentials) + + mocker.patch.object(boto3, 'session', boto3_double) + args = copy(dummy_credentials) + args["on_missing"] = 'skip' + retval = lookup_loader.get('amazon.aws.aws_secret').run(["missing_secret"], None, **args) + assert(retval == []) + + mocker.patch.object(boto3, 'session', boto3_double) + args = copy(dummy_credentials) + args["on_missing"] = 'warn' + retval = lookup_loader.get('amazon.aws.aws_secret').run(["missing_secret"], None, **args) + assert(retval == []) + + +def test_on_denied_option(mocker, dummy_credentials): boto3_double = mocker.MagicMock() - boto3_double.Session.return_value.client.return_value.get_secret_value.side_effect = ClientError(error_response, operation_name) + boto3_double.Session.return_value.client.return_value.get_secret_value.side_effect = ClientError(error_response_denied, operation_name) - with pytest.raises(AnsibleError): + with pytest.raises(AnsibleError, match="AccessDenied"): mocker.patch.object(boto3, 'session', boto3_double) - lookup_loader.get('amazon.aws.aws_secret').run(["denied_variable"], None, **dummy_credentials) + lookup_loader.get('amazon.aws.aws_secret').run(["denied_secret"], None, **dummy_credentials) + + mocker.patch.object(boto3, 'session', boto3_double) + args = copy(dummy_credentials) + args["on_denied"] = 'skip' + retval = lookup_loader.get('amazon.aws.aws_secret').run(["denied_secret"], None, **args) + assert(retval == []) + + mocker.patch.object(boto3, 'session', boto3_double) + args = copy(dummy_credentials) + args["on_denied"] = 'warn' + retval = lookup_loader.get('amazon.aws.aws_secret').run(["denied_secret"], None, **args) + assert(retval == [])