1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

[PR #7989/03966624 backport][stable-9] Consul implement agent service and check (#8513)

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 <felix@fontein.de>

* Update plugins/modules/consul_agent_check.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/consul_agent_check.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* 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 <felix@fontein.de>
Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
(cherry picked from commit 03966624ba)

Co-authored-by: Ilgmi <michael.ilg@mailbox.org>
This commit is contained in:
patchback[bot] 2024-06-16 10:10:45 +02:00 committed by GitHub
parent d6168a196b
commit 2d26fba0b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 810 additions and 21 deletions

2
.github/BOTMETA.yml vendored
View file

@ -1501,7 +1501,7 @@ macros:
team_ansible_core: team_ansible_core:
team_aix: MorrisA bcoca d-little flynn1973 gforster kairoaraujo marvin-sinister mator molekuul ramooncamacho wtcross 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_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_cyberark_conjur: jvanderhoof ryanprior
team_e_spirit: MatrixCrawler getjack team_e_spirit: MatrixCrawler getjack
team_flatpak: JayKayy oolongbrothers team_flatpak: JayKayy oolongbrothers

View file

@ -6,6 +6,8 @@
requires_ansible: '>=2.13.0' requires_ansible: '>=2.13.0'
action_groups: action_groups:
consul: consul:
- consul_agent_check
- consul_agent_service
- consul_auth_method - consul_auth_method
- consul_binding_rule - consul_binding_rule
- consul_policy - consul_policy

View file

@ -10,6 +10,7 @@ __metaclass__ = type
import copy import copy
import json import json
import re
from ansible.module_utils.six.moves.urllib import error as urllib_error from ansible.module_utils.six.moves.urllib import error as urllib_error
from ansible.module_utils.six.moves.urllib.parse import urlencode from ansible.module_utils.six.moves.urllib.parse import urlencode
@ -68,6 +69,25 @@ def camel_case_key(key):
return "".join(parts) return "".join(parts)
def validate_check(check):
validate_duration_keys = ['Interval', 'Ttl', 'Timeout']
validate_tcp_regex = r"(?P<host>.*):(?P<port>(?:[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_PARAMETER = "state"
STATE_PRESENT = "present" STATE_PRESENT = "present"
STATE_ABSENT = "absent" STATE_ABSENT = "absent"
@ -81,7 +101,7 @@ OPERATION_DELETE = "remove"
def _normalize_params(params, arg_spec): def _normalize_params(params, arg_spec):
final_params = {} final_params = {}
for k, v in params.items(): 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 continue
spec = arg_spec[k] spec = arg_spec[k]
if ( if (
@ -105,9 +125,10 @@ class _ConsulModule:
""" """
api_endpoint = None # type: str api_endpoint = None # type: str
unique_identifier = None # type: str unique_identifiers = None # type: list
result_key = None # type: str result_key = None # type: str
create_only_fields = set() create_only_fields = set()
operational_attributes = set()
params = {} params = {}
def __init__(self, module): def __init__(self, module):
@ -119,6 +140,8 @@ class _ConsulModule:
if k not in STATE_PARAMETER and k not in AUTH_ARGUMENTS_SPEC if k not in STATE_PARAMETER and k not in AUTH_ARGUMENTS_SPEC
} }
self.operational_attributes.update({"CreateIndex", "CreateTime", "Hash", "ModifyIndex"})
def execute(self): def execute(self):
obj = self.read_object() obj = self.read_object()
@ -203,14 +226,24 @@ class _ConsulModule:
return False return False
def prepare_object(self, existing, obj): def prepare_object(self, existing, obj):
operational_attributes = {"CreateIndex", "CreateTime", "Hash", "ModifyIndex"}
existing = { 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(): for k, v in obj.items():
existing[k] = v existing[k] = v
return existing 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): def endpoint_url(self, operation, identifier=None):
if operation == OPERATION_CREATE: if operation == OPERATION_CREATE:
return self.api_endpoint return self.api_endpoint
@ -219,7 +252,8 @@ class _ConsulModule:
raise RuntimeError("invalid arguments passed") raise RuntimeError("invalid arguments passed")
def read_object(self): 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: try:
return self.get(url) return self.get(url)
except RequestError as e: except RequestError as e:
@ -233,25 +267,28 @@ class _ConsulModule:
if self._module.check_mode: if self._module.check_mode:
return obj return obj
else: 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): 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) merged_object = self.prepare_object(existing, obj)
if self._module.check_mode: if self._module.check_mode:
return merged_object return merged_object
else: 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): def delete_object(self, obj):
if self._module.check_mode: if self._module.check_mode:
return {} return {}
else: else:
url = self.endpoint_url( url = self.endpoint_url(OPERATION_DELETE, self.id_from_obj(obj, camel_case=True))
OPERATION_DELETE, obj.get(camel_case_key(self.unique_identifier))
)
return self.delete(url) return self.delete(url)
def _request(self, method, url_parts, data=None, params=None): def _request(self, method, url_parts, data=None, params=None):
@ -309,7 +346,9 @@ class _ConsulModule:
if 400 <= status < 600: if 400 <= status < 600:
raise RequestError(status, response_data) raise RequestError(status, response_data)
if response_data:
return json.loads(response_data) return json.loads(response_data)
return None
def get(self, url_parts, **kwargs): def get(self, url_parts, **kwargs):
return self._request("GET", url_parts, **kwargs) return self._request("GET", url_parts, **kwargs)

View file

@ -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()

View file

@ -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()

View file

@ -168,7 +168,7 @@ def normalize_ttl(ttl):
class ConsulAuthMethodModule(_ConsulModule): class ConsulAuthMethodModule(_ConsulModule):
api_endpoint = "acl/auth-method" api_endpoint = "acl/auth-method"
result_key = "auth_method" result_key = "auth_method"
unique_identifier = "name" unique_identifiers = ["name"]
def map_param(self, k, v, is_update): def map_param(self, k, v, is_update):
if k == "config" and v: if k == "config" and v:

View file

@ -124,7 +124,7 @@ from ansible_collections.community.general.plugins.module_utils.consul import (
class ConsulBindingRuleModule(_ConsulModule): class ConsulBindingRuleModule(_ConsulModule):
api_endpoint = "acl/binding-rule" api_endpoint = "acl/binding-rule"
result_key = "binding_rule" result_key = "binding_rule"
unique_identifier = "id" unique_identifiers = ["id"]
def read_object(self): def read_object(self):
url = "acl/binding-rules?authmethod={0}".format(self.params["auth_method"]) url = "acl/binding-rules?authmethod={0}".format(self.params["auth_method"])

View file

@ -145,7 +145,7 @@ _ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
class ConsulPolicyModule(_ConsulModule): class ConsulPolicyModule(_ConsulModule):
api_endpoint = "acl/policy" api_endpoint = "acl/policy"
result_key = "policy" result_key = "policy"
unique_identifier = "id" unique_identifiers = ["id"]
def endpoint_url(self, operation, identifier=None): def endpoint_url(self, operation, identifier=None):
if operation == OPERATION_READ: if operation == OPERATION_READ:

View file

@ -212,7 +212,7 @@ from ansible_collections.community.general.plugins.module_utils.consul import (
class ConsulRoleModule(_ConsulModule): class ConsulRoleModule(_ConsulModule):
api_endpoint = "acl/role" api_endpoint = "acl/role"
result_key = "role" result_key = "role"
unique_identifier = "id" unique_identifiers = ["id"]
def endpoint_url(self, operation, identifier=None): def endpoint_url(self, operation, identifier=None):
if operation == OPERATION_READ: if operation == OPERATION_READ:

View file

@ -235,13 +235,13 @@ def normalize_link_obj(api_obj, module_obj, key):
class ConsulTokenModule(_ConsulModule): class ConsulTokenModule(_ConsulModule):
api_endpoint = "acl/token" api_endpoint = "acl/token"
result_key = "token" result_key = "token"
unique_identifier = "accessor_id" unique_identifiers = ["accessor_id"]
create_only_fields = {"expiration_ttl"} create_only_fields = {"expiration_ttl"}
def read_object(self): def read_object(self):
# if `accessor_id` is not supplied we can only create objects and are not idempotent # 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 None
return super(ConsulTokenModule, self).read_object() return super(ConsulTokenModule, self).read_object()

View file

@ -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

View file

@ -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'

View file

@ -97,6 +97,8 @@
- import_tasks: consul_token.yml - import_tasks: consul_token.yml
- import_tasks: consul_auth_method.yml - import_tasks: consul_auth_method.yml
- import_tasks: consul_binding_rule.yml - import_tasks: consul_binding_rule.yml
- import_tasks: consul_agent_service.yml
- import_tasks: consul_agent_check.yml
module_defaults: module_defaults:
group/community.general.consul: group/community.general.consul:
token: "{{ consul_management_token }}" token: "{{ consul_management_token }}"