Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ssm_parameter: add support for tags (#1573) #1575

2 changes: 2 additions & 0 deletions changelogs/fragments/1574-ssm-parameter-support-for-tags.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- ssm_parameter - add support for tags in ssm parameters (https://github.com/ansible-collections/community.aws/issues/1573).
120 changes: 117 additions & 3 deletions plugins/modules/ssm_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@
- amazon.aws.aws
- amazon.aws.ec2
- amazon.aws.boto3
- amazon.aws.tags

notes:
- Support for I(tags) and I(purge_tags) was added in release 5.3.0.

'''
mikehas marked this conversation as resolved.
Show resolved Hide resolved

EXAMPLES = '''
Expand Down Expand Up @@ -137,6 +142,29 @@
- name: recommend to use with aws_ssm lookup plugin
ansible.builtin.debug:
msg: "{{ lookup('amazon.aws.aws_ssm', 'Hello') }}"

- name: Create or update key/value pair in AWS SSM parameter store w/ tags
community.aws.ssm_parameter:
name: "Hello"
description: "This is your first key"
value: "World"
tags:
Environment: "dev"
Version: "1.0"
Confidentiality: "low"
Tag With Space: "foo bar"

- name: Add or update a tag on an existing parameter w/o removing existing tags
community.aws.ssm_parameter:
name: "Hello"
purge_tags: false
tags:
Contact: "person1"

- name: Delete all tags on an existing parameter
community.aws.ssm_parameter:
name: "Hello"
tags: {}
'''

RETURN = '''
Expand Down Expand Up @@ -208,12 +236,19 @@
description: Parameter version number
example: 3
returned: success
tags:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markuman What if we only return tags as a dictionary rather than a list of dicts and remove tags_dict from the response?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@markuman @alinabuzachis To provide a little context, tags_dict was copied over from plugins/modules/secretsmanager_secret.py. I'd be happy to remove the code if you'd like.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

secretsmanager_secret has some compatibility code in there because it originally returned the "boto3 style" (list of dicts) format, rather than the normal simple dictionary.

We often just convert the resource objects the APIs return from CamelCase to snake_case, and add that as part of what the module returns. When AWS suddenly adds support for Tags to a resource and changes what the API returns, we start returning the list-of-dict style tags, and need to go through a deprecation cycle before we can return the simple dict.

Since returning tags is new to this module, we can skip the "list-of-dict" and just return the "dict" format as "tags".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(There are also some cases where reviews simply missed that the module originally returned the boto3 style tags)

  • We're trying to tidy this up :)

description: A dictionary representing the tags associated with the parameter.
type: dict
returned: when the parameter has tags
example: {'MyTagName': 'Some Value'}
mikehas marked this conversation as resolved.
Show resolved Hide resolved
version_added: 5.3.0
'''

import time

try:
import botocore
from botocore.exceptions import BotoCoreError, ClientError
except ImportError:
pass # Handled by AnsibleAWSModule

Expand All @@ -223,6 +258,9 @@
from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry
from ansible_collections.community.aws.plugins.module_utils.base import BaseWaiterFactory
from ansible_collections.amazon.aws.plugins.module_utils.tagging import ansible_dict_to_boto3_tag_list
from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict
from ansible_collections.amazon.aws.plugins.module_utils.tagging import compare_aws_tags


class ParameterWaiterFactory(BaseWaiterFactory):
Expand Down Expand Up @@ -301,6 +339,58 @@ def _wait_deleted(client, module, name):
module.fail_json_aws(e, msg="Failed to describe parameter while waiting for deletion")


def tag_parameter(client, module, parameter_name, tags):
try:
return client.add_tags_to_resource(aws_retry=True, ResourceType='Parameter',
ResourceId=parameter_name, Tags=tags)
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg="Failed to add tag(s) to parameter")


def untag_parameter(client, module, parameter_name, tag_keys):
try:
return client.remove_tags_from_resource(aws_retry=True, ResourceType='Parameter',
ResourceId=parameter_name, TagKeys=tag_keys)
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg="Failed to remove tag(s) from parameter")


def get_parameter_tags(client, module, parameter_name):
try:
tags = client.list_tags_for_resource(aws_retry=True, ResourceType='Parameter',
ResourceId=parameter_name)['TagList']
tags_dict = boto3_tag_list_to_ansible_dict(tags)
return tags_dict
except (BotoCoreError, ClientError) as e:
module.fail_json_aws(e, msg="Unable to retrieve parameter tags")


def update_parameter_tags(client, module, parameter_name, supplied_tags):
changed = False
response = {}

if supplied_tags is None:
return False, response

current_tags = get_parameter_tags(client, module, parameter_name)
tags_to_add, tags_to_remove = compare_aws_tags(current_tags, supplied_tags,
module.params.get('purge_tags'))

if tags_to_add:
if module.check_mode:
return True, response
response = tag_parameter(client, module, parameter_name,
ansible_dict_to_boto3_tag_list(tags_to_add))
changed = True
if tags_to_remove:
if module.check_mode:
return True, response
response = untag_parameter(client, module, parameter_name, tags_to_remove)
changed = True

return changed, response


def update_parameter(client, module, **args):
changed = False
response = {}
Expand All @@ -310,8 +400,8 @@ def update_parameter(client, module, **args):
try:
response = client.put_parameter(aws_retry=True, **args)
changed = True
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="setting parameter")
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as exc:
module.fail_json_aws(exc, msg="setting parameter")

return changed, response

Expand All @@ -324,6 +414,9 @@ def describe_parameter(client, module, **args):
if not existing_parameter['Parameters']:
return None

tags_dict = get_parameter_tags(client, module, module.params.get('name'))
existing_parameter['Parameters'][0]['tags'] = tags_dict

return existing_parameter['Parameters'][0]


Expand Down Expand Up @@ -387,7 +480,25 @@ def create_update_parameter(client, module):
(changed, response) = update_parameter(client, module, **args)
if changed:
_wait_updated(client, module, module.params.get('name'), original_version)

# Handle tag updates for existing parameters
if module.params.get('overwrite_value') != 'never':
tags_changed, tags_response = update_parameter_tags(
client, module, existing_parameter['Parameter']['Name'],
module.params.get('tags'))

changed = changed or tags_changed

if tags_response:
response['tag_updates'] = tags_response

else:
# Add tags in initial creation request
if module.params.get('tags'):
args.update(Tags=ansible_dict_to_boto3_tag_list(module.params.get('tags')))
# Overwrite=True conflicts with tags and is not needed for new param
args.update(Overwrite=False)

(changed, response) = update_parameter(client, module, **args)
_wait_exists(client, module, module.params.get('name'))

Expand Down Expand Up @@ -444,6 +555,8 @@ def setup_module_object():
key_id=dict(default="alias/aws/ssm"),
overwrite_value=dict(default='changed', choices=['never', 'changed', 'always']),
tier=dict(default='Standard', choices=['Standard', 'Advanced', 'Intelligent-Tiering']),
tags=dict(type='dict', aliases=['resource_tags']),
purge_tags=dict(type='bool', default=True),
markuman marked this conversation as resolved.
Show resolved Hide resolved
)

return AnsibleAWSModule(
Expand Down Expand Up @@ -474,7 +587,8 @@ def main():
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="to describe parameter")
if parameter_metadata:
result['parameter_metadata'] = camel_dict_to_snake_dict(parameter_metadata)
result['parameter_metadata'] = camel_dict_to_snake_dict(parameter_metadata,
ignore_list=['tags'])

module.exit_json(changed=changed, **result)

Expand Down
Loading