From 03966624ba1e647238d8807a8da89615760d1068 Mon Sep 17 00:00:00 2001 From: Ilgmi Date: Sun, 16 Jun 2024 09:32:55 +0200 Subject: [PATCH] Consul implement agent service and check (#7989) * Implement agent service and check (#7987) * implement update of service and check * update tests update documentation * update documentation * add consul_agent_check/service to action_groups check if unique_identifier of name is in params to get object add suggested improvements * update sanity * fix sanity issues update documentation * fix naming * fix naming check if response_data has data * fix sanity extra-docs * add as ignore maintainer in BOTMETA.yml update version_added to 8.4 * fix sanity * add to maintainers * Update plugins/modules/consul_agent_check.py Co-authored-by: Felix Fontein * Update plugins/modules/consul_agent_check.py Co-authored-by: Felix Fontein * Update plugins/modules/consul_agent_check.py Co-authored-by: Felix Fontein * update version_added * if create and update return no object as result we read the object again * get_first_appearing_identifier check the params for the given identifier and return it to simplify id vs name * add unique_identifiers as a new property and a method to decide which identifier should be used * fix sanity * add self to team consul remove params with no values add operational_attributes that inherited classes can set them get identifier value from object * fix sanity fix test * remove the possibility to add checks with consul_agent_check. check if service has changed * remove tests for idempotency check because for checks it is not possible * remove unique_identifier from consul.py change unique_identifier to unique_identifiers * get id from params * Revert "remove unique_identifier from consul.py" This reverts commit a4f0d0220dd23e95871914b152c25ff352097a2c. * update version to 8.5 * Revert "Revert "remove unique_identifier from consul.py"" This reverts commit d2c35cf04c8aaf5f0175d772f862a796e22e35d4. * update description update test * fix sanity tests * fix sanity tests * update documentation for agent_check * fix line length * add documentation * fix sanity * simplified check for Tcp Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * check duration with regex * fix * update documentation --------- Co-authored-by: Felix Fontein Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> --- .github/BOTMETA.yml | 2 +- meta/runtime.yml | 2 + plugins/module_utils/consul.py | 67 +++- plugins/modules/consul_agent_check.py | 254 +++++++++++++++ plugins/modules/consul_agent_service.py | 289 ++++++++++++++++++ plugins/modules/consul_auth_method.py | 2 +- plugins/modules/consul_binding_rule.py | 2 +- plugins/modules/consul_policy.py | 2 +- plugins/modules/consul_role.py | 2 +- plugins/modules/consul_token.py | 4 +- .../consul/tasks/consul_agent_check.yml | 114 +++++++ .../consul/tasks/consul_agent_service.yml | 89 ++++++ .../integration/targets/consul/tasks/main.yml | 2 + 13 files changed, 810 insertions(+), 21 deletions(-) create mode 100644 plugins/modules/consul_agent_check.py create mode 100644 plugins/modules/consul_agent_service.py create mode 100644 tests/integration/targets/consul/tasks/consul_agent_check.yml create mode 100644 tests/integration/targets/consul/tasks/consul_agent_service.yml diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 36d667706a..faedb42605 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -1501,7 +1501,7 @@ macros: team_ansible_core: team_aix: MorrisA bcoca d-little flynn1973 gforster kairoaraujo marvin-sinister mator molekuul ramooncamacho wtcross team_bsd: JoergFiedler MacLemon bcoca dch jasperla mekanix opoplawski overhacked tuxillo - team_consul: sgargan apollo13 + team_consul: sgargan apollo13 Ilgmi team_cyberark_conjur: jvanderhoof ryanprior team_e_spirit: MatrixCrawler getjack team_flatpak: JayKayy oolongbrothers diff --git a/meta/runtime.yml b/meta/runtime.yml index edeb53005f..4f5007b4a4 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -6,6 +6,8 @@ requires_ansible: '>=2.13.0' action_groups: consul: + - consul_agent_check + - consul_agent_service - consul_auth_method - consul_binding_rule - consul_policy diff --git a/plugins/module_utils/consul.py b/plugins/module_utils/consul.py index 68c1a130b4..cd54a105f8 100644 --- a/plugins/module_utils/consul.py +++ b/plugins/module_utils/consul.py @@ -10,6 +10,7 @@ __metaclass__ = type import copy import json +import re from ansible.module_utils.six.moves.urllib import error as urllib_error from ansible.module_utils.six.moves.urllib.parse import urlencode @@ -68,6 +69,25 @@ def camel_case_key(key): return "".join(parts) +def validate_check(check): + validate_duration_keys = ['Interval', 'Ttl', 'Timeout'] + validate_tcp_regex = r"(?P.*):(?P(?:[0-9]+))$" + if check.get('Tcp') is not None: + match = re.match(validate_tcp_regex, check['Tcp']) + if not match: + raise Exception('tcp check must be in host:port format') + for duration in validate_duration_keys: + if duration in check and check[duration] is not None: + check[duration] = validate_duration(check[duration]) + + +def validate_duration(duration): + if duration: + if not re.search(r"\d+(?:ns|us|ms|s|m|h)", duration): + duration = "{0}s".format(duration) + return duration + + STATE_PARAMETER = "state" STATE_PRESENT = "present" STATE_ABSENT = "absent" @@ -81,7 +101,7 @@ OPERATION_DELETE = "remove" def _normalize_params(params, arg_spec): final_params = {} for k, v in params.items(): - if k not in arg_spec: # Alias + if k not in arg_spec or v is None: # Alias continue spec = arg_spec[k] if ( @@ -105,9 +125,10 @@ class _ConsulModule: """ api_endpoint = None # type: str - unique_identifier = None # type: str + unique_identifiers = None # type: list result_key = None # type: str create_only_fields = set() + operational_attributes = set() params = {} def __init__(self, module): @@ -119,6 +140,8 @@ class _ConsulModule: if k not in STATE_PARAMETER and k not in AUTH_ARGUMENTS_SPEC } + self.operational_attributes.update({"CreateIndex", "CreateTime", "Hash", "ModifyIndex"}) + def execute(self): obj = self.read_object() @@ -203,14 +226,24 @@ class _ConsulModule: return False def prepare_object(self, existing, obj): - operational_attributes = {"CreateIndex", "CreateTime", "Hash", "ModifyIndex"} existing = { - k: v for k, v in existing.items() if k not in operational_attributes + k: v for k, v in existing.items() if k not in self.operational_attributes } for k, v in obj.items(): existing[k] = v return existing + def id_from_obj(self, obj, camel_case=False): + def key_func(key): + return camel_case_key(key) if camel_case else key + + if self.unique_identifiers: + for identifier in self.unique_identifiers: + identifier = key_func(identifier) + if identifier in obj: + return obj[identifier] + return None + def endpoint_url(self, operation, identifier=None): if operation == OPERATION_CREATE: return self.api_endpoint @@ -219,7 +252,8 @@ class _ConsulModule: raise RuntimeError("invalid arguments passed") def read_object(self): - url = self.endpoint_url(OPERATION_READ, self.params.get(self.unique_identifier)) + identifier = self.id_from_obj(self.params) + url = self.endpoint_url(OPERATION_READ, identifier) try: return self.get(url) except RequestError as e: @@ -233,25 +267,28 @@ class _ConsulModule: if self._module.check_mode: return obj else: - return self.put(self.api_endpoint, data=self.prepare_object({}, obj)) + url = self.endpoint_url(OPERATION_CREATE) + created_obj = self.put(url, data=self.prepare_object({}, obj)) + if created_obj is None: + created_obj = self.read_object() + return created_obj def update_object(self, existing, obj): - url = self.endpoint_url( - OPERATION_UPDATE, existing.get(camel_case_key(self.unique_identifier)) - ) merged_object = self.prepare_object(existing, obj) if self._module.check_mode: return merged_object else: - return self.put(url, data=merged_object) + url = self.endpoint_url(OPERATION_UPDATE, self.id_from_obj(existing, camel_case=True)) + updated_obj = self.put(url, data=merged_object) + if updated_obj is None: + updated_obj = self.read_object() + return updated_obj def delete_object(self, obj): if self._module.check_mode: return {} else: - url = self.endpoint_url( - OPERATION_DELETE, obj.get(camel_case_key(self.unique_identifier)) - ) + url = self.endpoint_url(OPERATION_DELETE, self.id_from_obj(obj, camel_case=True)) return self.delete(url) def _request(self, method, url_parts, data=None, params=None): @@ -309,7 +346,9 @@ class _ConsulModule: if 400 <= status < 600: raise RequestError(status, response_data) - return json.loads(response_data) + if response_data: + return json.loads(response_data) + return None def get(self, url_parts, **kwargs): return self._request("GET", url_parts, **kwargs) diff --git a/plugins/modules/consul_agent_check.py b/plugins/modules/consul_agent_check.py new file mode 100644 index 0000000000..3739260049 --- /dev/null +++ b/plugins/modules/consul_agent_check.py @@ -0,0 +1,254 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, Michael Ilg +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +module: consul_agent_check +short_description: Add, modify, and delete checks within a consul cluster +version_added: 9.1.0 +description: + - Allows the addition, modification and deletion of checks in a consul + cluster via the agent. For more details on using and configuring Checks, + see U(https://developer.hashicorp.com/consul/api-docs/agent/check). + - Currently, there is no complete way to retrieve the script, interval or TTL + metadata for a registered check. Without this metadata it is not possible to + tell if the data supplied with ansible represents a change to a check. As a + result this does not attempt to determine changes and will always report a + changed occurred. An API method is planned to supply this metadata so at that + stage change management will be added. +author: + - Michael Ilg (@Ilgmi) +extends_documentation_fragment: + - community.general.consul + - community.general.consul.actiongroup_consul + - community.general.consul.token + - community.general.attributes +attributes: + check_mode: + support: full + details: + - The result is the object as it is defined in the module options and not the object structure of the consul API. + For a better overview of what the object structure looks like, + take a look at U(https://developer.hashicorp.com/consul/api-docs/agent/check#list-checks). + diff_mode: + support: partial + details: + - In check mode the diff will show the object as it is defined in the module options and not the object structure of the consul API. +options: + state: + description: + - Whether the check should be present or absent. + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Required name for the service check. + type: str + id: + description: + - Specifies a unique ID for this check on the node. This defaults to the O(name) parameter, but it may be necessary to provide + an ID for uniqueness. This value will return in the response as "CheckId". + type: str + interval: + description: + - The interval at which the service check will be run. + This is a number with a V(s) or V(m) suffix to signify the units of seconds or minutes, for example V(15s) or V(1m). + If no suffix is supplied V(s) will be used by default, for example V(10) will be V(10s). + - Required if one of the parameters O(args), O(http), or O(tcp) is specified. + type: str + notes: + description: + - Notes to attach to check when registering it. + type: str + args: + description: + - Specifies command arguments to run to update the status of the check. + - Requires O(interval) to be provided. + - Mutually exclusive with O(ttl), O(tcp) and O(http). + type: list + elements: str + ttl: + description: + - Checks can be registered with a TTL instead of a O(args) and O(interval) + this means that the service will check in with the agent before the + TTL expires. If it doesn't the check will be considered failed. + Required if registering a check and the script an interval are missing + Similar to the interval this is a number with a V(s) or V(m) suffix to + signify the units of seconds or minutes, for example V(15s) or V(1m). + If no suffix is supplied V(s) will be used by default, for example V(10) will be V(10s). + - Mutually exclusive with O(args), O(tcp) and O(http). + type: str + tcp: + description: + - Checks can be registered with a TCP port. This means that consul + will check if the connection attempt to that port is successful (that is, the port is currently accepting connections). + The format is V(host:port), for example V(localhost:80). + - Requires O(interval) to be provided. + - Mutually exclusive with O(args), O(ttl) and O(http). + type: str + version_added: '1.3.0' + http: + description: + - Checks can be registered with an HTTP endpoint. This means that consul + will check that the http endpoint returns a successful HTTP status. + - Requires O(interval) to be provided. + - Mutually exclusive with O(args), O(ttl) and O(tcp). + type: str + timeout: + description: + - A custom HTTP check timeout. The consul default is 10 seconds. + Similar to the interval this is a number with a V(s) or V(m) suffix to + signify the units of seconds or minutes, for example V(15s) or V(1m). + If no suffix is supplied V(s) will be used by default, for example V(10) will be V(10s). + type: str + service_id: + description: + - The ID for the service, must be unique per node. If O(state=absent), + defaults to the service name if supplied. + type: str +''' + +EXAMPLES = ''' +- name: Register tcp check for service 'nginx' + community.general.consul_agent_check: + name: nginx_tcp_check + service_id: nginx + interval: 60s + tcp: localhost:80 + notes: "Nginx Check" + +- name: Register http check for service 'nginx' + community.general.consul_agent_check: + name: nginx_http_check + service_id: nginx + interval: 60s + http: http://localhost:80/status + notes: "Nginx Check" + +- name: Remove check for service 'nginx' + community.general.consul_agent_check: + state: absent + id: nginx_http_check + service_id: "{{ nginx_service.ID }}" +''' + +RETURN = """ +check: + description: The check as returned by the consul HTTP API. + returned: always + type: dict + sample: + CheckID: nginx_check + ServiceID: nginx + Interval: 30s + Type: http + Notes: Nginx Check +operation: + description: The operation performed. + returned: changed + type: str + sample: update +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.consul import ( + AUTH_ARGUMENTS_SPEC, + OPERATION_CREATE, + OPERATION_UPDATE, + OPERATION_DELETE, + OPERATION_READ, + _ConsulModule, + validate_check, +) + +_ARGUMENT_SPEC = { + "state": dict(default="present", choices=["present", "absent"]), + "name": dict(type='str'), + "id": dict(type='str'), + "interval": dict(type='str'), + "notes": dict(type='str'), + "args": dict(type='list', elements='str'), + "http": dict(type='str'), + "tcp": dict(type='str'), + "ttl": dict(type='str'), + "timeout": dict(type='str'), + "service_id": dict(type='str'), +} + +_MUTUALLY_EXCLUSIVE = [ + ('args', 'ttl', 'tcp', 'http'), +] + +_REQUIRED_IF = [ + ('state', 'present', ['name']), + ('state', 'absent', ('id', 'name'), True), +] + +_REQUIRED_BY = { + 'args': 'interval', + 'http': 'interval', + 'tcp': 'interval', +} + +_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC) + + +class ConsulAgentCheckModule(_ConsulModule): + api_endpoint = "agent/check" + result_key = "check" + unique_identifiers = ["id", "name"] + operational_attributes = {"Node", "CheckID", "Output", "ServiceName", "ServiceTags", + "Status", "Type", "ExposedPort", "Definition"} + + def endpoint_url(self, operation, identifier=None): + if operation == OPERATION_READ: + return "agent/checks" + if operation in [OPERATION_CREATE, OPERATION_UPDATE]: + return "/".join([self.api_endpoint, "register"]) + if operation == OPERATION_DELETE: + return "/".join([self.api_endpoint, "deregister", identifier]) + + return super(ConsulAgentCheckModule, self).endpoint_url(operation, identifier) + + def read_object(self): + url = self.endpoint_url(OPERATION_READ) + checks = self.get(url) + identifier = self.id_from_obj(self.params) + if identifier in checks: + return checks[identifier] + return None + + def prepare_object(self, existing, obj): + existing = super(ConsulAgentCheckModule, self).prepare_object(existing, obj) + validate_check(existing) + return existing + + def delete_object(self, obj): + if not self._module.check_mode: + self.put(self.endpoint_url(OPERATION_DELETE, obj.get("CheckID"))) + return {} + + +def main(): + module = AnsibleModule( + _ARGUMENT_SPEC, + mutually_exclusive=_MUTUALLY_EXCLUSIVE, + required_if=_REQUIRED_IF, + required_by=_REQUIRED_BY, + supports_check_mode=True, + ) + + consul_module = ConsulAgentCheckModule(module) + consul_module.execute() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/consul_agent_service.py b/plugins/modules/consul_agent_service.py new file mode 100644 index 0000000000..a8ef098970 --- /dev/null +++ b/plugins/modules/consul_agent_service.py @@ -0,0 +1,289 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2024, Michael Ilg +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = ''' +module: consul_agent_service +short_description: Add, modify and delete services within a consul cluster +version_added: 9.1.0 +description: + - Allows the addition, modification and deletion of services in a consul + cluster via the agent. + - There are currently no plans to create services and checks in one. + This is because the Consul API does not provide checks for a service and + the checks themselves do not match the module parameters. + Therefore, only a service without checks can be created in this module. +author: + - Michael Ilg (@Ilgmi) +extends_documentation_fragment: + - community.general.consul + - community.general.consul.actiongroup_consul + - community.general.consul.token + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: partial + details: + - In check mode the diff will miss operational attributes. +options: + state: + description: + - Whether the service should be present or absent. + choices: ['present', 'absent'] + default: present + type: str + name: + description: + - Unique name for the service on a node, must be unique per node, + required if registering a service. + type: str + id: + description: + - Specifies a unique ID for this service. This must be unique per agent. This defaults to the O(name) parameter if not provided. + If O(state=absent), defaults to the service name if supplied. + type: str + tags: + description: + - Tags that will be attached to the service registration. + type: list + elements: str + address: + description: + - The address to advertise that the service will be listening on. + This value will be passed as the C(address) parameter to Consul's + C(/v1/agent/service/register) API method, so refer to the Consul API + documentation for further details. + type: str + meta: + description: + - Optional meta data used for filtering. + For keys, the characters C(A-Z), C(a-z), C(0-9), C(_), C(-) are allowed. + Not allowed characters are replaced with underscores. + type: dict + service_port: + description: + - The port on which the service is listening. Can optionally be supplied for + registration of a service, that is if O(name) or O(id) is set. + type: int + enable_tag_override: + description: + - Specifies to disable the anti-entropy feature for this service's tags. + If EnableTagOverride is set to true then external agents can update this service in the catalog and modify the tags. + type: bool + default: False + weights: + description: + - Specifies weights for the service + type: dict + suboptions: + passing: + description: + - Weights for passing. + type: int + default: 1 + warning: + description: + - Weights for warning. + type: int + default: 1 + default: {"passing": 1, "warning": 1} +''' + +EXAMPLES = ''' +- name: Register nginx service with the local consul agent + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: nginx + service_port: 80 + +- name: Register nginx with a tcp check + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: nginx + service_port: 80 + +- name: Register nginx with an http check + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: nginx + service_port: 80 + +- name: Register external service nginx available at 10.1.5.23 + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: nginx + service_port: 80 + address: 10.1.5.23 + +- name: Register nginx with some service tags + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: nginx + service_port: 80 + tags: + - prod + - webservers + +- name: Register nginx with some service meta + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: nginx + service_port: 80 + meta: + nginx_version: 1.25.3 + +- name: Remove nginx service + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + service_id: nginx + state: absent + +- name: Register celery worker service + community.general.consul_agent_service: + host: consul1.example.com + token: some_management_acl + name: celery-worker + tags: + - prod + - worker +''' + +RETURN = """ +service: + description: The service as returned by the consul HTTP API. + returned: always + type: dict + sample: + ID: nginx + Service: nginx + Address: localhost + Port: 80 + Tags: + - http + Meta: + - nginx_version: 1.23.3 + Datacenter: dc1 + Weights: + Passing: 1 + Warning: 1 + ContentHash: 61a245cd985261ac + EnableTagOverride: false +operation: + description: The operation performed. + returned: changed + type: str + sample: update +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.general.plugins.module_utils.consul import ( + AUTH_ARGUMENTS_SPEC, + OPERATION_CREATE, + OPERATION_UPDATE, + OPERATION_DELETE, + _ConsulModule +) + +_CHECK_MUTUALLY_EXCLUSIVE = [('args', 'ttl', 'tcp', 'http')] +_CHECK_REQUIRED_BY = { + 'args': 'interval', + 'http': 'interval', + 'tcp': 'interval', +} + +_ARGUMENT_SPEC = { + "state": dict(default="present", choices=["present", "absent"]), + "name": dict(type='str'), + "id": dict(type='str'), + "tags": dict(type='list', elements='str'), + "address": dict(type='str'), + "meta": dict(type='dict'), + "service_port": dict(type='int'), + "enable_tag_override": dict(type='bool', default=False), + "weights": dict(type='dict', options=dict( + passing=dict(type='int', default=1, no_log=False), + warning=dict(type='int', default=1) + ), default={"passing": 1, "warning": 1}) +} + +_REQUIRED_IF = [ + ('state', 'present', ['name']), + ('state', 'absent', ('id', 'name'), True), +] + +_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC) + + +class ConsulAgentServiceModule(_ConsulModule): + api_endpoint = "agent/service" + result_key = "service" + unique_identifiers = ["id", "name"] + operational_attributes = {"Service", "ContentHash", "Datacenter"} + + def endpoint_url(self, operation, identifier=None): + if operation in [OPERATION_CREATE, OPERATION_UPDATE]: + return "/".join([self.api_endpoint, "register"]) + if operation == OPERATION_DELETE: + return "/".join([self.api_endpoint, "deregister", identifier]) + + return super(ConsulAgentServiceModule, self).endpoint_url(operation, identifier) + + def prepare_object(self, existing, obj): + existing = super(ConsulAgentServiceModule, self).prepare_object(existing, obj) + if "ServicePort" in existing: + existing["Port"] = existing.pop("ServicePort") + + if "ID" not in existing: + existing["ID"] = existing["Name"] + + return existing + + def needs_update(self, api_obj, module_obj): + obj = {} + if "Service" in api_obj: + obj["Service"] = api_obj["Service"] + api_obj = self.prepare_object(api_obj, obj) + + if "Name" in module_obj: + module_obj["Service"] = module_obj.pop("Name") + if "ServicePort" in module_obj: + module_obj["Port"] = module_obj.pop("ServicePort") + + return super(ConsulAgentServiceModule, self).needs_update(api_obj, module_obj) + + def delete_object(self, obj): + if not self._module.check_mode: + url = self.endpoint_url(OPERATION_DELETE, self.id_from_obj(obj, camel_case=True)) + self.put(url) + return {} + + +def main(): + module = AnsibleModule( + _ARGUMENT_SPEC, + required_if=_REQUIRED_IF, + supports_check_mode=True, + ) + + consul_module = ConsulAgentServiceModule(module) + consul_module.execute() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/consul_auth_method.py b/plugins/modules/consul_auth_method.py index afe549f6ef..e28474c313 100644 --- a/plugins/modules/consul_auth_method.py +++ b/plugins/modules/consul_auth_method.py @@ -168,7 +168,7 @@ def normalize_ttl(ttl): class ConsulAuthMethodModule(_ConsulModule): api_endpoint = "acl/auth-method" result_key = "auth_method" - unique_identifier = "name" + unique_identifiers = ["name"] def map_param(self, k, v, is_update): if k == "config" and v: diff --git a/plugins/modules/consul_binding_rule.py b/plugins/modules/consul_binding_rule.py index 88496f8675..6a2882cee2 100644 --- a/plugins/modules/consul_binding_rule.py +++ b/plugins/modules/consul_binding_rule.py @@ -124,7 +124,7 @@ from ansible_collections.community.general.plugins.module_utils.consul import ( class ConsulBindingRuleModule(_ConsulModule): api_endpoint = "acl/binding-rule" result_key = "binding_rule" - unique_identifier = "id" + unique_identifiers = ["id"] def read_object(self): url = "acl/binding-rules?authmethod={0}".format(self.params["auth_method"]) diff --git a/plugins/modules/consul_policy.py b/plugins/modules/consul_policy.py index 2ed6021b03..36139ac097 100644 --- a/plugins/modules/consul_policy.py +++ b/plugins/modules/consul_policy.py @@ -145,7 +145,7 @@ _ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC) class ConsulPolicyModule(_ConsulModule): api_endpoint = "acl/policy" result_key = "policy" - unique_identifier = "id" + unique_identifiers = ["id"] def endpoint_url(self, operation, identifier=None): if operation == OPERATION_READ: diff --git a/plugins/modules/consul_role.py b/plugins/modules/consul_role.py index e07e2036fe..d6c4e4dd92 100644 --- a/plugins/modules/consul_role.py +++ b/plugins/modules/consul_role.py @@ -212,7 +212,7 @@ from ansible_collections.community.general.plugins.module_utils.consul import ( class ConsulRoleModule(_ConsulModule): api_endpoint = "acl/role" result_key = "role" - unique_identifier = "id" + unique_identifiers = ["id"] def endpoint_url(self, operation, identifier=None): if operation == OPERATION_READ: diff --git a/plugins/modules/consul_token.py b/plugins/modules/consul_token.py index 02bc544da7..c8bc8bc279 100644 --- a/plugins/modules/consul_token.py +++ b/plugins/modules/consul_token.py @@ -235,13 +235,13 @@ def normalize_link_obj(api_obj, module_obj, key): class ConsulTokenModule(_ConsulModule): api_endpoint = "acl/token" result_key = "token" - unique_identifier = "accessor_id" + unique_identifiers = ["accessor_id"] create_only_fields = {"expiration_ttl"} def read_object(self): # if `accessor_id` is not supplied we can only create objects and are not idempotent - if not self.params.get(self.unique_identifier): + if not self.id_from_obj(self.params): return None return super(ConsulTokenModule, self).read_object() diff --git a/tests/integration/targets/consul/tasks/consul_agent_check.yml b/tests/integration/targets/consul/tasks/consul_agent_check.yml new file mode 100644 index 0000000000..e1229c794f --- /dev/null +++ b/tests/integration/targets/consul/tasks/consul_agent_check.yml @@ -0,0 +1,114 @@ +--- +# Copyright (c) 2024, Michael Ilg (@Ilgmi) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create a service + community.general.consul_agent_service: + name: nginx + service_port: 80 + address: localhost + tags: + - http + meta: + nginx_version: 1.25.3 + register: result + +- set_fact: + nginx_service: "{{result.service}}" + +- assert: + that: + - result is changed + - result.service.ID is defined + +- name: Add a check for service + community.general.consul_agent_check: + name: nginx_check + id: nginx_check + interval: 30s + http: http://localhost:80/morestatus + notes: "Nginx Check" + service_id: "{{ nginx_service.ID }}" + register: result + +- assert: + that: + - result is changed + - result.check is defined + - result.check.CheckID == 'nginx_check' + - result.check.ServiceID == 'nginx' + - result.check.Interval == '30s' + - result.check.Type == 'http' + - result.check.Notes == 'Nginx Check' + +- set_fact: + nginx_service_check: "{{ result.check }}" + +- name: Update check for service + community.general.consul_agent_check: + name: "{{ nginx_service_check.Name }}" + id: "{{ nginx_service_check.CheckID }}" + interval: 60s + http: http://localhost:80/morestatus + notes: "New Nginx Check" + service_id: "{{ nginx_service.ID }}" + register: result + +- assert: + that: + - result is changed + - result.check is defined + - result.check.CheckID == 'nginx_check' + - result.check.ServiceID == 'nginx' + - result.check.Interval == '1m0s' + - result.check.Type == 'http' + - result.check.Notes == 'New Nginx Check' + +- name: Remove check + community.general.consul_agent_check: + id: "{{ nginx_service_check.Name }}" + state: absent + service_id: "{{ nginx_service.ID }}" + register: result + +- assert: + that: + - result is changed + - result is not failed + - result.operation == 'remove' + +- name: Add a check + community.general.consul_agent_check: + name: check + id: check + interval: 30s + tcp: localhost:80 + notes: "check" + register: result + +- assert: + that: + - result is changed + - result.check is defined + +- name: Update a check + community.general.consul_agent_check: + name: check + id: check + interval: 60s + tcp: localhost:80 + notes: "check" + register: result + +- assert: + that: + - result is changed + - result.check is defined + - result.check.Interval == '1m0s' + +- name: Remove check + community.general.consul_agent_check: + id: check + state: absent + register: result \ No newline at end of file diff --git a/tests/integration/targets/consul/tasks/consul_agent_service.yml b/tests/integration/targets/consul/tasks/consul_agent_service.yml new file mode 100644 index 0000000000..95270f74b3 --- /dev/null +++ b/tests/integration/targets/consul/tasks/consul_agent_service.yml @@ -0,0 +1,89 @@ +--- +# Copyright (c) 2024, Michael Ilg (@Ilgmi) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Create a service + community.general.consul_agent_service: + name: nginx + service_port: 80 + address: localhost + tags: + - http + meta: + nginx_version: 1.25.3 + register: result + +- set_fact: + nginx_service: "{{result.service}}" + +- assert: + that: + - result is changed + - result.service.ID is defined + - result.service.Service == 'nginx' + - result.service.Address == 'localhost' + - result.service.Port == 80 + - result.service.Tags[0] == 'http' + - result.service.Meta.nginx_version is defined + - result.service.Meta.nginx_version == '1.25.3' + - result.service.ContentHash is defined + +- name: Update service + community.general.consul_agent_service: + id: "{{ nginx_service.ID }}" + name: "{{ nginx_service.Service }}" + service_port: 8080 + address: 127.0.0.1 + tags: + - http + - new_tag + meta: + nginx_version: 1.0.0 + nginx: 1.25.3 + register: result +- assert: + that: + - result is changed + - result.service.ID is defined + - result.service.Service == 'nginx' + - result.service.Address == '127.0.0.1' + - result.service.Port == 8080 + - result.service.Tags[0] == 'http' + - result.service.Tags[1] == 'new_tag' + - result.service.Meta.nginx_version is defined + - result.service.Meta.nginx_version == '1.0.0' + - result.service.Meta.nginx is defined + - result.service.Meta.nginx == '1.25.3' + - result.service.ContentHash is defined + +- name: Update service not changed when updating again without changes + community.general.consul_agent_service: + id: "{{ nginx_service.ID }}" + name: "{{ nginx_service.Service }}" + service_port: 8080 + address: 127.0.0.1 + tags: + - http + - new_tag + meta: + nginx_version: 1.0.0 + nginx: 1.25.3 + register: result + +- assert: + that: + - result is not changed + - result.operation is not defined + +- name: Remove service + community.general.consul_agent_service: + id: "{{ nginx_service.ID }}" + state: absent + register: result + +- assert: + that: + - result is changed + - result is not failed + - result.operation == 'remove' \ No newline at end of file diff --git a/tests/integration/targets/consul/tasks/main.yml b/tests/integration/targets/consul/tasks/main.yml index 6fef2b9980..0ac58fc40e 100644 --- a/tests/integration/targets/consul/tasks/main.yml +++ b/tests/integration/targets/consul/tasks/main.yml @@ -97,6 +97,8 @@ - import_tasks: consul_token.yml - import_tasks: consul_auth_method.yml - import_tasks: consul_binding_rule.yml + - import_tasks: consul_agent_service.yml + - import_tasks: consul_agent_check.yml module_defaults: group/community.general.consul: token: "{{ consul_management_token }}"