diff --git a/changelogs/fragments/455-lookup_aws_secret-deleted.yml b/changelogs/fragments/455-lookup_aws_secret-deleted.yml new file mode 100644 index 00000000000..fda4b8b45dc --- /dev/null +++ b/changelogs/fragments/455-lookup_aws_secret-deleted.yml @@ -0,0 +1,2 @@ +minor_changes: +- aws_secret - added support for gracefully handling deleted secrets (https://github.com/ansible-collections/amazon.aws/pull/455). diff --git a/plugins/lookup/aws_secret.py b/plugins/lookup/aws_secret.py index 83664c28ada..eb80dd5e9e4 100644 --- a/plugins/lookup/aws_secret.py +++ b/plugins/lookup/aws_secret.py @@ -49,6 +49,16 @@ - No effect when used with I(bypath). type: boolean default: false + on_deleted: + description: + - Action to take if the secret has been marked for deletion. + - C(error) will raise a fatal error when the secret has been marked for deletion. + - C(skip) will silently ignore the deleted secret. + - C(warn) will skip over the deleted secret but issue a warning. + default: error + type: string + choices: ['error', 'skip', 'warn'] + version_added: 2.0.0 on_missing: description: - Action to take if the secret is missing. @@ -125,6 +135,7 @@ from ansible.plugins.lookup import LookupBase from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code +from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_message from ansible_collections.amazon.aws.plugins.module_utils.ec2 import HAS_BOTO3 @@ -148,7 +159,7 @@ class LookupModule(LookupBase): def run(self, terms, variables=None, boto_profile=None, aws_profile=None, aws_secret_key=None, aws_access_key=None, aws_security_token=None, region=None, bypath=False, nested=False, join=False, version_stage=None, version_id=None, on_missing='error', - on_denied='error'): + on_denied='error', on_deleted='error'): ''' :arg terms: a list of lookups to run. e.g. ['parameter_name', 'parameter_name_too' ] @@ -164,12 +175,17 @@ def run(self, terms, variables=None, boto_profile=None, aws_profile=None, :kwarg version_stage: Stage of the secret version :kwarg version_id: Version of the secret(s) :kwarg on_missing: Action to take if the secret is missing + :kwarg on_deleted: Action to take if the secret is marked for deletion :kwarg on_denied: Action to take if access to the secret is denied :returns: A list of parameter values or a list of dictionaries if bypath=True. ''' if not HAS_BOTO3: raise AnsibleError('botocore and boto3 are required for aws_ssm lookup.') + deleted = on_deleted.lower() + if not isinstance(deleted, string_types) or deleted not in ['error', 'warn', 'skip']: + raise AnsibleError('"on_deleted" must be a string and one of "error", "warn" or "skip", not %s' % deleted) + missing = on_missing.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) @@ -217,7 +233,8 @@ def run(self, terms, variables=None, boto_profile=None, aws_profile=None, for term in terms: value = self.get_secret_value(term, client, version_stage=version_stage, version_id=version_id, - on_missing=missing, on_denied=denied, nested=nested) + on_missing=missing, on_denied=denied, on_deleted=deleted, + nested=nested) if value: secrets.append(value) if join: @@ -227,7 +244,7 @@ def run(self, terms, variables=None, boto_profile=None, aws_profile=None, return secrets - def get_secret_value(self, term, client, version_stage=None, version_id=None, on_missing=None, on_denied=None, nested=False): + def get_secret_value(self, term, client, version_stage=None, version_id=None, on_missing=None, on_denied=None, on_deleted=None, nested=False): params = {} params['SecretId'] = term if version_id: @@ -258,7 +275,12 @@ def get_secret_value(self, term, client, version_stage=None, version_id=None, on return str(ret_val) else: return response['SecretString'] - except is_boto3_error_code('ResourceNotFoundException'): + except is_boto3_error_message('marked for deletion'): + if on_deleted == 'error': + raise AnsibleError("Failed to find secret %s (marked for deletion)" % term) + elif on_deleted == 'warn': + self._display.warning('Skipping, did not find secret (marked for deletion) %s' % term) + except is_boto3_error_code('ResourceNotFoundException'): # pylint: disable=duplicate-except if on_missing == 'error': raise AnsibleError("Failed to find secret %s (ResourceNotFound)" % term) elif on_missing == 'warn': diff --git a/tests/integration/targets/lookup_aws_secret/tasks/main.yaml b/tests/integration/targets/lookup_aws_secret/tasks/main.yaml index 0bf01101ed3..1966ec95fa5 100644 --- a/tests/integration/targets/lookup_aws_secret/tasks/main.yaml +++ b/tests/integration/targets/lookup_aws_secret/tasks/main.yaml @@ -18,20 +18,32 @@ block: - name: define secret name set_fact: - secret_name: "ansible-test-{{ resource_prefix | hash('md5') }}-secret" + secret_name: "ansible-test-{{ tiny_prefix }}-secret" secret_value: "{{ lookup('password', '/dev/null chars=ascii_lowercase,digits,punctuation length=16') }}" on_missing_secret: "skip" + on_deleted_secret: "skip" - - name: lookup missing secret + - name: lookup missing secret (skip) set_fact: - missing_secret: "{{ lookup('amazon.aws.aws_secret', secret_name, on_missing=on_missing_secret, **connection_args) }}" - + missing_secret: "{{ lookup('amazon.aws.aws_secret', secret_name, on_missing=on_missing_secret, on_deleted=on_deleted_secret, **connection_args) }}" + - name: assert that missing_secret is defined assert: that: - missing_secret is defined - missing_secret | list | length == 0 - + + - name: lookup missing secret (error) + set_fact: + missing_secret: "{{ lookup('amazon.aws.aws_secret', secret_name, **connection_args) }}" + ignore_errors: True + register: get_missing_secret + + - name: assert that setting the missing_secret failed + assert: + that: + - get_missing_secret is failed + - name: create secret "{{ secret_name }}" aws_secret: name: "{{ secret_name }}" @@ -39,20 +51,49 @@ tags: ansible-test: "aws-tests-integration" state: present - + - name: read secret value set_fact: look_secret: "{{ lookup('amazon.aws.aws_secret', secret_name, **connection_args) }}" - + - name: assert that secret was successfully retrieved assert: that: - look_secret == secret_value - + + - name: delete secret + aws_secret: + name: "{{ secret_name }}" + state: absent + recovery_window: 7 + + - name: lookup deleted secret (skip) + set_fact: + deleted_secret: "{{ lookup('amazon.aws.aws_secret', secret_name, on_missing=on_missing_secret, on_deleted=on_deleted_secret, **connection_args) }}" + + - name: assert that deleted_secret is defined + assert: + that: + - deleted_secret is defined + - deleted_secret | list | length == 0 + + - name: lookup deleted secret (error) + set_fact: + missing_secret: "{{ lookup('amazon.aws.aws_secret', secret_name, **connection_args) }}" + ignore_errors: True + register: get_deleted_secret + + - name: assert that setting the deleted_secret failed + assert: + that: + - get_deleted_secret is failed + always: - # delete secret created - - name: delete secret - aws_secret: - name: "{{ secret_name }}" - state: absent - ignore_errors: yes + + # delete secret created + - name: delete secret + aws_secret: + name: "{{ secret_name }}" + state: absent + recovery_window: 0 + ignore_errors: yes