Skip to content

Commit

Permalink
route53_health_check: Add feature to create multiple health checks wi…
Browse files Browse the repository at this point in the history
…thout updating existing health check (#1143) (#1324)

[PR #1143/8d3fec8e backport][stable-4] route53_health_check: Add feature to create multiple health checks without updating existing health check

This is a backport of PR #1143 as merged into main (8d3fec8).
SUMMARY

Might fix #88


Update [06/30/2022]:
This PR adds three new parameters to route53_health_check module.
health_check_id=dict(type='str', aliases=['id'], required=False),
health_check_name=dict(type='str', aliases=['name'], required=False),
use_unique_names=dict(type='bool', required=False),


health_check_id (alias id): when set, can be used to update and also delete health checks.
use_unique_names and health_check_name (alias name): these are used together to make use of health check name as a unique identifier to create/update/delete health check and also assign a name to the health check.


Update [05/20/2022]:
Based on discussion between 15 May to 19 May below, the approach of adding ignore_existing parameter has been discarded for the time being.
The progress on this WIP PR put on hold until route53_info return value issue gets resolved. #1236

Added parameter ignore_existing that allows to create multiple route53 healthchecks with similar parameters instead of updating existing healthcheck (example: create multiple route53 healthchecks with different paths to same server).
ISSUE TYPE


Feature Pull Request

COMPONENT NAME

route53_health_check
ADDITIONAL INFORMATION


Consider following example from the issue.

- name: set up route53 health checks
  connection: local
  become: false
  route53_health_check:
    failure_threshold: 3
    ip_address: '{{ ip_address }}'
    port: 80
    request_interval: 30
    resource_path: '{{ item }}'
    state: present
    type: HTTP
    #ignore_existing: True
  register: result
  with_items:
    - /some_status1
    - /other_status2

With ignore_exisitng parameter, it will create two healthchecks to same sever with different resource_path.
Without ignore_existing parameter, it will create first healthcheck to sever, then start creating the second healthcheck but as it's having same server/ip_address, it updates the existing healthcheck instead, so creates only single healthcheck at the end of the task.

Reviewed-by: Mark Chappell <None>
  • Loading branch information
patchback[bot] authored Jul 7, 2022
1 parent 809698a commit 33522ca
Show file tree
Hide file tree
Showing 6 changed files with 606 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
minor_changes:
- route53_health_check - Added new parameter ``health_check_id`` with alias ``id`` to allow update and delete health check by ID (https://github.com/ansible-collections/community.aws/pull/1143).
- route53_health_check - Added new parameter ``use_unique_names`` used with new parameter ``health_check_name`` with alias ``name`` to set health check name as unique identifier (https://github.com/ansible-collections/community.aws/pull/1143).
175 changes: 157 additions & 18 deletions plugins/modules/route53_health_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
description:
- The type of health check that you want to create, which indicates how
Amazon Route 53 determines whether an endpoint is healthy.
required: true
- Once health_check is created, type can not be changed.
choices: [ 'HTTP', 'HTTPS', 'HTTP_STR_MATCH', 'HTTPS_STR_MATCH', 'TCP' ]
type: str
resource_path:
Expand Down Expand Up @@ -86,6 +86,28 @@
- Will default to C(3) if not specified on creation.
choices: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
type: int
health_check_name:
description:
- Name of the Health Check.
- Used together with I(use_unique_names) to set/make use of I(health_check_name) as a unique identifier.
type: str
required: False
aliases: ['name']
version_added: 4.1.0
use_unique_names:
description:
- Used together with I(health_check_name) to set/make use of I(health_check_name) as a unique identifier.
type: bool
required: False
version_added: 4.1.0
health_check_id:
description:
- ID of the health check to be update or deleted.
- If provided, a health check can be updated or deleted based on the ID as unique identifier.
type: str
required: False
aliases: ['id']
version_added: 4.1.0
author:
- "zimbatm (@zimbatm)"
notes:
Expand Down Expand Up @@ -120,10 +142,35 @@
weight: 100
health_check: "{{ my_health_check.health_check.id }}"
- name: create a simple health check with health_check_name as unique identifier
community.aws.route53_health_check:
state: present
health_check_name: ansible
fqdn: ansible.com
port: 443
type: HTTPS
use_unique_names: true
- name: Delete health-check
community.aws.route53_health_check:
state: absent
fqdn: host1.example.com
- name: Update Health check by ID - update ip_address
community.aws.route53_health_check:
id: 12345678-abcd-abcd-abcd-0fxxxxxxxxxx
ip_address: 1.2.3.4
- name: Update Health check by ID - update port
community.aws.route53_health_check:
id: 12345678-abcd-abcd-abcd-0fxxxxxxxxxx
ip_address: 8080
- name: Delete Health check by ID
community.aws.route53_health_check:
state: absent
id: 12345678-abcd-abcd-abcd-0fxxxxxxxxxx
'''

RETURN = r'''
Expand Down Expand Up @@ -249,7 +296,6 @@ def find_health_check(ip_addr, fqdn, hc_type, request_interval, port):
# Additionally, we can't properly wrap the paginator, so retrying means
# starting from scratch with a paginator
results = _list_health_checks()

while True:
for check in results.get('HealthChecks'):
config = check.get('HealthCheckConfig')
Expand All @@ -268,6 +314,20 @@ def find_health_check(ip_addr, fqdn, hc_type, request_interval, port):
return None


def get_existing_checks_with_name():
results = _list_health_checks()
health_checks_with_name = {}
while True:
for check in results.get('HealthChecks'):
if 'Name' in describe_health_check(check['Id'])['tags']:
check_name = describe_health_check(check['Id'])['tags']['Name']
health_checks_with_name[check_name] = check
if results.get('IsTruncated', False):
results = _list_health_checks(Marker=results.get('NextMarker'))
else:
return health_checks_with_name


def delete_health_check(check_id):
if not check_id:
return False, None
Expand Down Expand Up @@ -348,10 +408,14 @@ def create_health_check(ip_addr_in, fqdn_in, type_in, request_interval_in, port_


def update_health_check(existing_check):
# In theory it's also possible to update the IPAddress, Port and
# FullyQualifiedDomainName, however, because we use these in lieu of a
# 'Name' to uniquely identify the health check this isn't currently
# supported. If we accepted an ID it would be possible to modify them.
# It's possible to update following parameters
# - ResourcePath
# - SearchString
# - FailureThreshold
# - Disabled
# - IPAddress
# - Port
# - FullyQualifiedDomainName

changes = dict()
existing_config = existing_check.get('HealthCheckConfig')
Expand All @@ -372,10 +436,23 @@ def update_health_check(existing_check):
if disabled is not None and disabled != existing_config.get('Disabled'):
changes['Disabled'] = module.params.get('disabled')

# If updating based on Health Check ID or health_check_name, we can update
if module.params.get('health_check_id') or module.params.get('use_unique_names'):
ip_address = module.params.get('ip_address', None)
if ip_address is not None and ip_address != existing_config.get('IPAddress'):
changes['IPAddress'] = module.params.get('ip_address')

port = module.params.get('port', None)
if port is not None and port != existing_config.get('Port'):
changes['Port'] = module.params.get('port')

fqdn = module.params.get('fqdn', None)
if fqdn is not None and fqdn != existing_config.get('FullyQualifiedDomainName'):
changes['FullyQualifiedDomainName'] = module.params.get('fqdn')

# No changes...
if not changes:
return False, None

if module.check_mode:
return True, 'update'

Expand Down Expand Up @@ -419,31 +496,44 @@ def main():
disabled=dict(type='bool'),
ip_address=dict(),
port=dict(type='int'),
type=dict(required=True, choices=['HTTP', 'HTTPS', 'HTTP_STR_MATCH', 'HTTPS_STR_MATCH', 'TCP']),
type=dict(choices=['HTTP', 'HTTPS', 'HTTP_STR_MATCH', 'HTTPS_STR_MATCH', 'TCP']),
resource_path=dict(),
fqdn=dict(),
string_match=dict(),
request_interval=dict(type='int', choices=[10, 30], default=30),
failure_threshold=dict(type='int', choices=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
tags=dict(type='dict', aliases=['resource_tags']),
purge_tags=dict(type='bool'),
health_check_id=dict(type='str', aliases=['id'], required=False),
health_check_name=dict(type='str', aliases=['name'], required=False),
use_unique_names=dict(type='bool', required=False),
)

args_one_of = [
['ip_address', 'fqdn'],
['ip_address', 'fqdn', 'health_check_id'],
]

args_if = [
['type', 'TCP', ('port',)],
]

args_required_together = [
['use_unique_names', 'health_check_name'],
]

args_mutually_exclusive = [
['health_check_id', 'health_check_name']
]

global module
global client

module = AnsibleAWSModule(
argument_spec=argument_spec,
required_one_of=args_one_of,
required_if=args_if,
required_together=args_required_together,
mutually_exclusive=args_mutually_exclusive,
supports_check_mode=True,
)

Expand All @@ -455,6 +545,9 @@ def main():
version='5.0.0', collection_name='community.aws')
module.params['purge_tags'] = False

if not module.params.get('health_check_id') and not module.params.get('type'):
module.fail_json(msg="parameter 'type' is required if not updating or deleting health check by ID.")

state_in = module.params.get('state')
ip_addr_in = module.params.get('ip_address')
port_in = module.params.get('port')
Expand All @@ -464,6 +557,8 @@ def main():
string_match_in = module.params.get('string_match')
request_interval_in = module.params.get('request_interval')
failure_threshold_in = module.params.get('failure_threshold')
health_check_name = module.params.get('health_check_name')
tags = module.params.get('tags')

# Default port
if port_in is None:
Expand All @@ -484,22 +579,66 @@ def main():
action = None
check_id = None

existing_check = find_health_check(ip_addr_in, fqdn_in, type_in, request_interval_in, port_in)

if existing_check:
check_id = existing_check.get('Id')

if module.params.get('use_unique_names') or module.params.get('health_check_id'):
module.deprecate(
'The health_check_name is currently non required parameter.'
' This behavior will change and health_check_name '
' will change to required=True and use_unique_names will change to default=True in release 6.0.0.',
version='6.0.0', collection_name='community.aws')

# If update or delete Health Check based on ID
update_delete_by_id = False
if module.params.get('health_check_id'):
update_delete_by_id = True
id_to_update_delete = module.params.get('health_check_id')
try:
existing_check = client.get_health_check(HealthCheckId=id_to_update_delete)['HealthCheck']
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
module.exit_json(changed=False, msg='The specified health check with ID: {0} does not exist'.format(id_to_update_delete))
else:
existing_check = find_health_check(ip_addr_in, fqdn_in, type_in, request_interval_in, port_in)
if existing_check:
check_id = existing_check.get('Id')

# Delete Health Check
if state_in == 'absent':
changed, action = delete_health_check(check_id)
if update_delete_by_id:
changed, action = delete_health_check(id_to_update_delete)
else:
changed, action = delete_health_check(check_id)
check_id = None

# Create Health Check
elif state_in == 'present':
if existing_check is None:
if existing_check is None and not module.params.get('use_unique_names') and not update_delete_by_id:
changed, action, check_id = create_health_check(ip_addr_in, fqdn_in, type_in, request_interval_in, port_in)

# Update Health Check
else:
changed, action = update_health_check(existing_check)
# If health_check_name is a unique identifier
if module.params.get('use_unique_names'):
existing_checks_with_name = get_existing_checks_with_name()
# update the health_check if another health check with same name exists
if health_check_name in existing_checks_with_name:
changed, action = update_health_check(existing_checks_with_name[health_check_name])
else:
# create a new health_check if another health check with same name does not exists
changed, action, check_id = create_health_check(ip_addr_in, fqdn_in, type_in, request_interval_in, port_in)
# Add tag to add name to health check
if check_id:
if not tags:
tags = {}
tags['Name'] = health_check_name

else:
if update_delete_by_id:
changed, action = update_health_check(existing_check)
else:
changed, action = update_health_check(existing_check)

if check_id:
changed |= manage_tags(module, client, 'healthcheck', check_id,
module.params.get('tags'), module.params.get('purge_tags'))
tags, module.params.get('purge_tags'))

health_check = describe_health_check(id=check_id)
health_check['action'] = action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#ip_address: We allocate an EIP due to route53 restrictions
fqdn: '{{ tiny_prefix }}.route53-health.ansible.test'
fqdn_1: '{{ tiny_prefix }}-1.route53-health.ansible.test'
port: 8080
type: 'TCP'
request_interval: 30
Expand All @@ -27,7 +28,9 @@ failure_threshold_updated: 1
# for string_match we need an _STR_MATCH type
type_https_match: 'HTTPS_STR_MATCH'
type_http_match: 'HTTP_STR_MATCH'
type_http: 'HTTP'
resource_path: '/health.php'
resource_path_1: '/new-health.php'
resource_path_updated: '/healthz'
string_match: 'Hello'
string_match_updated: 'Hello World'
Loading

0 comments on commit 33522ca

Please sign in to comment.