[PR #7878/29f98654 backport][stable-8] Add new consul modules and reuse code between them. ()

Add new consul modules and reuse code between them. ()

Refactored consul modules and added new roles.

(cherry picked from commit 29f9865497)

Co-authored-by: Florian Apolloner <florian@apolloner.eu>
This commit is contained in:
patchback[bot] 2024-01-27 10:33:33 +01:00 committed by GitHub
parent 1ee2bba140
commit 0a904d60cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1508 additions and 568 deletions

2
.github/BOTMETA.yml vendored
View file

@ -1494,7 +1494,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 team_consul: sgargan apollo13
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

@ -1,2 +1,7 @@
minor_changes: minor_changes:
- 'consul_policy, consul_role, consul_session - removed dependency on ``requests`` and factored out common parts (https://github.com/ansible-collections/community.general/pull/7826).' - 'consul_policy, consul_role, consul_session - removed dependency on ``requests`` and factored out common parts (https://github.com/ansible-collections/community.general/pull/7826, https://github.com/ansible-collections/community.general/pull/7878).'
- consul_policy - added support for diff and check mode (https://github.com/ansible-collections/community.general/pull/7878).
- consul_role - added support for diff mode (https://github.com/ansible-collections/community.general/pull/7878).
- consul_role - added support for templated policies (https://github.com/ansible-collections/community.general/pull/7878).
- consul_role - ``service_identities`` now expects a ``service_name`` option to match the Consul API, the old ``name`` is still supported as alias (https://github.com/ansible-collections/community.general/pull/7878).
- consul_role - ``node_identities`` now expects a ``node_name`` option to match the Consul API, the old ``name`` is still supported as alias (https://github.com/ansible-collections/community.general/pull/7878).

View file

@ -5,6 +5,7 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
@ -33,12 +34,16 @@ options:
description: description:
- Whether to verify the TLS certificate of the consul agent. - Whether to verify the TLS certificate of the consul agent.
default: true default: true
token:
description:
- The token to use for authorization.
type: str
ca_path: ca_path:
description: description:
- The CA bundle to use for https connections - The CA bundle to use for https connections
type: str type: str
""" """
TOKEN = r"""
options:
token:
description:
- The token to use for authorization.
type: str
"""

View file

@ -5,8 +5,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import copy
import json import json
from ansible.module_utils.six.moves.urllib import error as urllib_error from ansible.module_utils.six.moves.urllib import error as urllib_error
@ -15,35 +17,84 @@ from ansible.module_utils.urls import open_url
def get_consul_url(configuration): def get_consul_url(configuration):
return '%s://%s:%s/v1' % (configuration.scheme, return "%s://%s:%s/v1" % (
configuration.host, configuration.port) configuration.scheme,
configuration.host,
configuration.port,
)
def get_auth_headers(configuration): def get_auth_headers(configuration):
if configuration.token is None: if configuration.token is None:
return {} return {}
else: else:
return {'X-Consul-Token': configuration.token} return {"X-Consul-Token": configuration.token}
class RequestError(Exception): class RequestError(Exception):
pass def __init__(self, status, response_data=None):
self.status = status
self.response_data = response_data
def __str__(self):
if self.response_data is None:
# self.status is already the message (backwards compat)
return self.status
return "HTTP %d: %s" % (self.status, self.response_data)
def handle_consul_response_error(response): def handle_consul_response_error(response):
if 400 <= response.status_code < 600: if 400 <= response.status_code < 600:
raise RequestError('%d %s' % (response.status_code, response.content)) raise RequestError("%d %s" % (response.status_code, response.content))
def auth_argument_spec(): AUTH_ARGUMENTS_SPEC = dict(
return dict( host=dict(default="localhost"),
host=dict(default="localhost"), port=dict(type="int", default=8500),
port=dict(type="int", default=8500), scheme=dict(default="http"),
scheme=dict(default="http"), validate_certs=dict(type="bool", default=True),
validate_certs=dict(type="bool", default=True), token=dict(no_log=True),
token=dict(no_log=True), ca_path=dict(),
ca_path=dict(), )
)
def camel_case_key(key):
parts = []
for part in key.split("_"):
if part in {"id", "ttl", "jwks", "jwt", "oidc", "iam", "sts"}:
parts.append(part.upper())
else:
parts.append(part.capitalize())
return "".join(parts)
STATE_PARAMETER = "state"
STATE_PRESENT = "present"
STATE_ABSENT = "absent"
OPERATION_READ = "read"
OPERATION_CREATE = "create"
OPERATION_UPDATE = "update"
OPERATION_DELETE = "remove"
def _normalize_params(params, arg_spec):
final_params = {}
for k, v in params.items():
if k not in arg_spec: # Alias
continue
spec = arg_spec[k]
if (
spec.get("type") == "list"
and spec.get("elements") == "dict"
and spec.get("options")
and v
):
v = [_normalize_params(d, spec["options"]) for d in v]
elif spec.get("type") == "dict" and spec.get("options") and v:
v = _normalize_params(v, spec["options"])
final_params[k] = v
return final_params
class _ConsulModule: class _ConsulModule:
@ -53,13 +104,160 @@ class _ConsulModule:
As such backwards incompatible changes can occur even in bugfix releases. As such backwards incompatible changes can occur even in bugfix releases.
""" """
api_endpoint = None # type: str
unique_identifier = None # type: str
result_key = None # type: str
create_only_fields = set()
params = {}
def __init__(self, module): def __init__(self, module):
self.module = module self._module = module
self.params = _normalize_params(module.params, module.argument_spec)
self.api_params = {
k: camel_case_key(k)
for k in self.params
if k not in STATE_PARAMETER and k not in AUTH_ARGUMENTS_SPEC
}
def execute(self):
obj = self.read_object()
changed = False
diff = {}
if self.params[STATE_PARAMETER] == STATE_PRESENT:
obj_from_module = self.module_to_obj(obj is not None)
if obj is None:
operation = OPERATION_CREATE
new_obj = self.create_object(obj_from_module)
diff = {"before": {}, "after": new_obj}
changed = True
else:
operation = OPERATION_UPDATE
if self._needs_update(obj, obj_from_module):
new_obj = self.update_object(obj, obj_from_module)
diff = {"before": obj, "after": new_obj}
changed = True
else:
new_obj = obj
elif self.params[STATE_PARAMETER] == STATE_ABSENT:
operation = OPERATION_DELETE
if obj is not None:
self.delete_object(obj)
changed = True
diff = {"before": obj, "after": {}}
else:
diff = {"before": {}, "after": {}}
new_obj = None
else:
raise RuntimeError("Unknown state supplied.")
result = {"changed": changed}
if changed:
result["operation"] = operation
if self._module._diff:
result["diff"] = diff
if self.result_key:
result[self.result_key] = new_obj
self._module.exit_json(**result)
def module_to_obj(self, is_update):
obj = {}
for k, v in self.params.items():
result = self.map_param(k, v, is_update)
if result:
obj[result[0]] = result[1]
return obj
def map_param(self, k, v, is_update):
def helper(item):
return {camel_case_key(k): v for k, v in item.items()}
def needs_camel_case(k):
spec = self._module.argument_spec[k]
return (
spec.get("type") == "list"
and spec.get("elements") == "dict"
and spec.get("options")
) or (spec.get("type") == "dict" and spec.get("options"))
if k in self.api_params and v is not None:
if isinstance(v, dict) and needs_camel_case(k):
v = helper(v)
elif isinstance(v, (list, tuple)) and needs_camel_case(k):
v = [helper(i) for i in v]
if is_update and k in self.create_only_fields:
return
return camel_case_key(k), v
def _needs_update(self, api_obj, module_obj):
api_obj = copy.deepcopy(api_obj)
module_obj = copy.deepcopy(module_obj)
return self.needs_update(api_obj, module_obj)
def needs_update(self, api_obj, module_obj):
for k, v in module_obj.items():
if k not in api_obj:
return True
if api_obj[k] != v:
return True
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
}
for k, v in obj.items():
existing[k] = v
return existing
def endpoint_url(self, operation, identifier=None):
if operation == OPERATION_CREATE:
return self.api_endpoint
elif identifier:
return "/".join([self.api_endpoint, identifier])
raise RuntimeError("invalid arguments passed")
def read_object(self):
url = self.endpoint_url(OPERATION_READ, self.params.get(self.unique_identifier))
try:
return self.get(url)
except RequestError as e:
if e.status == 404:
return
elif e.status == 403 and b"ACL not found" in e.response_data:
return
raise
def create_object(self, obj):
if self._module.check_mode:
return obj
else:
return self.put(self.api_endpoint, data=self.prepare_object({}, 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)
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))
)
return self.delete(url)
def _request(self, method, url_parts, data=None, params=None): def _request(self, method, url_parts, data=None, params=None):
module_params = self.module.params module_params = self.params
if isinstance(url_parts, str): if not isinstance(url_parts, (tuple, list)):
url_parts = [url_parts] url_parts = [url_parts]
if params: if params:
# Remove values that are None # Remove values that are None
@ -74,7 +272,7 @@ class _ConsulModule:
url = "/".join([base_url] + list(url_parts)) url = "/".join([base_url] + list(url_parts))
headers = {} headers = {}
token = self.module.params.get("token") token = self.params.get("token")
if token: if token:
headers["X-Consul-Token"] = token headers["X-Consul-Token"] = token
@ -93,19 +291,25 @@ class _ConsulModule:
ca_path=ca_path, ca_path=ca_path,
) )
response_data = response.read() response_data = response.read()
except urllib_error.URLError as e:
self.module.fail_json(
msg="Could not connect to consul agent at %s:%s, error was %s"
% (module_params["host"], module_params["port"], str(e))
)
else:
status = ( status = (
response.status if hasattr(response, "status") else response.getcode() response.status if hasattr(response, "status") else response.getcode()
) )
if 400 <= status < 600:
raise RequestError("%d %s" % (status, response_data))
return json.loads(response_data) except urllib_error.URLError as e:
if isinstance(e, urllib_error.HTTPError):
status = e.code
response_data = e.fp.read()
else:
self._module.fail_json(
msg="Could not connect to consul agent at %s:%s, error was %s"
% (module_params["host"], module_params["port"], str(e))
)
raise
if 400 <= status < 600:
raise RequestError(status, response_data)
return json.loads(response_data)
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,108 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = """
module: consul_acl_bootstrap
short_description: Bootstrap ACLs in Consul
version_added: 8.3.0
description:
- Allows bootstrapping of ACLs in a Consul cluster, see
U(https://developer.hashicorp.com/consul/api-docs/acl#bootstrap-acls) for details.
author:
- Florian Apolloner (@apollo13)
extends_documentation_fragment:
- community.general.consul
- community.general.attributes
attributes:
check_mode:
support: none
diff_mode:
support: none
options:
state:
description:
- Whether the token should be present or absent.
choices: ['present', 'bootstrapped']
default: present
type: str
bootstrap_secret:
description:
- The secret to be used as secret ID for the initial token.
- Needs to be an UUID.
type: str
"""
EXAMPLES = """
- name: Bootstrap the ACL system
community.general.consul_acl_bootstrap:
bootstrap_secret: 22eaeed1-bdbd-4651-724e-42ae6c43e387
"""
RETURN = """
result:
description:
- The bootstrap result as returned by the consul HTTP API.
- "B(Note:) If O(bootstrap_secret) has been specified the C(SecretID) and
C(ID) will not contain the secret but C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER).
If you pass O(bootstrap_secret), make sure your playbook/role does not depend
on this return value!"
returned: changed
type: dict
sample:
AccessorID: 834a5881-10a9-a45b-f63c-490e28743557
CreateIndex: 25
CreateTime: '2024-01-21T20:26:27.114612038+01:00'
Description: Bootstrap Token (Global Management)
Hash: X2AgaFhnQGRhSSF/h0m6qpX1wj/HJWbyXcxkEM/5GrY=
ID: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
Local: false
ModifyIndex: 25
Policies:
- ID: 00000000-0000-0000-0000-000000000001
Name: global-management
SecretID: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.consul import (
AUTH_ARGUMENTS_SPEC,
RequestError,
_ConsulModule,
)
_ARGUMENT_SPEC = {
"state": dict(type="str", choices=["present", "bootstrapped"], default="present"),
"bootstrap_secret": dict(type="str", no_log=True),
}
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
_ARGUMENT_SPEC.pop("token")
def main():
module = AnsibleModule(_ARGUMENT_SPEC)
consul_module = _ConsulModule(module)
data = {}
if "bootstrap_secret" in module.params:
data["BootstrapSecret"] = module.params["bootstrap_secret"]
try:
response = consul_module.put("acl/bootstrap", data=data)
except RequestError as e:
if e.status == 403 and b"ACL bootstrap no longer allowed" in e.response_data:
return module.exit_json(changed=False)
raise
else:
return module.exit_json(changed=True, result=response)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,206 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = """
module: consul_auth_method
short_description: Manipulate Consul auth methods
version_added: 8.3.0
description:
- Allows the addition, modification and deletion of auth methods in a consul
cluster via the agent. For more details on using and configuring ACLs,
see U(https://www.consul.io/docs/guides/acl.html).
author:
- Florian Apolloner (@apollo13)
extends_documentation_fragment:
- community.general.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 token should be present or absent.
choices: ['present', 'absent']
default: present
type: str
name:
description:
- Specifies a name for the ACL auth method.
- The name can contain alphanumeric characters, dashes C(-), and underscores C(_).
type: str
required: true
type:
description:
- The type of auth method being configured.
- This field is immutable.
- Required when the auth method is created.
type: str
choices: ['kubernetes', 'jwt', 'oidc', 'aws-iam']
description:
description:
- Free form human readable description of the auth method.
type: str
display_name:
description:
- An optional name to use instead of O(name) when displaying information about this auth method.
type: str
max_token_ttl:
description:
- This specifies the maximum life of any token created by this auth method.
- Can be specified in the form of V(60s) or V(5m) (that is, 60 seconds or 5 minutes, respectively).
type: str
token_locality:
description:
- Defines the kind of token that this auth method should produce.
type: str
choices: ['local', 'global']
config:
description:
- The raw configuration to use for the chosen auth method.
- Contents will vary depending upon the type chosen.
- Required when the auth method is created.
type: dict
"""
EXAMPLES = """
- name: Create an auth method
community.general.consul_auth_method:
name: test
type: jwt
config:
jwt_validation_pubkeys:
- |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----
token: "{{ consul_management_token }}"
- name: Delete auth method
community.general.consul_auth_method:
name: test
state: absent
token: "{{ consul_management_token }}"
"""
RETURN = """
auth_method:
description: The auth method as returned by the consul HTTP API.
returned: always
type: dict
sample:
Config:
JWTValidationPubkeys:
- |-
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----
CreateIndex: 416
ModifyIndex: 487
Name: test
Type: jwt
operation:
description: The operation performed.
returned: changed
type: str
sample: update
"""
import re
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.consul import (
AUTH_ARGUMENTS_SPEC,
_ConsulModule,
camel_case_key,
)
def normalize_ttl(ttl):
matches = re.findall(r"(\d+)(:h|m|s)", ttl)
ttl = 0
for value, unit in matches:
value = int(value)
if unit == "m":
value *= 60
elif unit == "h":
value *= 60 * 60
ttl += value
new_ttl = ""
hours, remainder = divmod(ttl, 3600)
if hours:
new_ttl += "{0}h".format(hours)
minutes, seconds = divmod(remainder, 60)
if minutes:
new_ttl += "{0}m".format(minutes)
if seconds:
new_ttl += "{0}s".format(seconds)
return new_ttl
class ConsulAuthMethodModule(_ConsulModule):
api_endpoint = "acl/auth-method"
result_key = "auth_method"
unique_identifier = "name"
def map_param(self, k, v, is_update):
if k == "config" and v:
v = {camel_case_key(k2): v2 for k2, v2 in v.items()}
return super(ConsulAuthMethodModule, self).map_param(k, v, is_update)
def needs_update(self, api_obj, module_obj):
if "MaxTokenTTL" in module_obj:
module_obj["MaxTokenTTL"] = normalize_ttl(module_obj["MaxTokenTTL"])
return super(ConsulAuthMethodModule, self).needs_update(api_obj, module_obj)
_ARGUMENT_SPEC = {
"name": dict(type="str", required=True),
"type": dict(type="str", choices=["kubernetes", "jwt", "oidc", "aws-iam"]),
"description": dict(type="str"),
"display_name": dict(type="str"),
"max_token_ttl": dict(type="str", no_log=False),
"token_locality": dict(type="str", choices=["local", "global"]),
"config": dict(type="dict"),
"state": dict(default="present", choices=["present", "absent"]),
}
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
def main():
module = AnsibleModule(
_ARGUMENT_SPEC,
supports_check_mode=True,
)
consul_module = ConsulAuthMethodModule(module)
consul_module.execute()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,182 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = """
module: consul_binding_rule
short_description: Manipulate Consul binding rules
version_added: 8.3.0
description:
- Allows the addition, modification and deletion of binding rules in a consul
cluster via the agent. For more details on using and configuring binding rules,
see U(https://developer.hashicorp.com/consul/api-docs/acl/binding-rules).
author:
- Florian Apolloner (@apollo13)
extends_documentation_fragment:
- community.general.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 binding rule should be present or absent.
choices: ['present', 'absent']
default: present
type: str
name:
description:
- Specifies a name for the binding rule.
- 'Note: This is used to identify the binding rule. But since the API does not support a name, it is prefixed to the description.'
type: str
required: true
description:
description:
- Free form human readable description of the binding rule.
type: str
auth_method:
description:
- The name of the auth method that this rule applies to.
type: str
required: true
selector:
description:
- Specifies the expression used to match this rule against valid identities returned from an auth method validation.
- If empty this binding rule matches all valid identities returned from the auth method.
type: str
bind_type:
description:
- Specifies the way the binding rule affects a token created at login.
type: str
choices: [service, node, role, templated-policy]
bind_name:
description:
- The name to bind to a token at login-time.
- What it binds to can be adjusted with different values of the O(bind_type) parameter.
type: str
bind_vars:
description:
- Specifies the templated policy variables when O(bind_type) is set to V(templated-policy).
type: dict
"""
EXAMPLES = """
- name: Create a binding rule
community.general.consul_binding_rule:
name: my_name
description: example rule
auth_method: minikube
bind_type: service
bind_name: "{{ serviceaccount.name }}"
token: "{{ consul_management_token }}"
- name: Remove a binding rule
community.general.consul_binding_rule:
name: my_name
auth_method: minikube
state: absent
"""
RETURN = """
binding_rule:
description: The binding rule as returned by the consul HTTP API.
returned: always
type: dict
sample:
Description: "my_name: example rule"
AuthMethod: minikube
Selector: serviceaccount.namespace==default
BindType: service
BindName: "{{ serviceaccount.name }}"
CreateIndex: 30
ID: 59c8a237-e481-4239-9202-45f117950c5f
ModifyIndex: 33
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,
RequestError,
_ConsulModule,
)
class ConsulBindingRuleModule(_ConsulModule):
api_endpoint = "acl/binding-rule"
result_key = "binding_rule"
unique_identifier = "id"
def read_object(self):
url = "acl/binding-rules?authmethod={0}".format(self.params["auth_method"])
try:
results = self.get(url)
for result in results:
if result.get("Description").startswith(
"{0}: ".format(self.params["name"])
):
return result
except RequestError as e:
if e.status == 404:
return
elif e.status == 403 and b"ACL not found" in e.response_data:
return
raise
def module_to_obj(self, is_update):
obj = super(ConsulBindingRuleModule, self).module_to_obj(is_update)
del obj["Name"]
return obj
def prepare_object(self, existing, obj):
final = super(ConsulBindingRuleModule, self).prepare_object(existing, obj)
name = self.params["name"]
description = final.pop("Description", "").split(": ", 1)[-1]
final["Description"] = "{0}: {1}".format(name, description)
return final
_ARGUMENT_SPEC = {
"name": dict(type="str", required=True),
"description": dict(type="str"),
"auth_method": dict(type="str", required=True),
"selector": dict(type="str"),
"bind_type": dict(
type="str", choices=["service", "node", "role", "templated-policy"]
),
"bind_name": dict(type="str"),
"bind_vars": dict(type="dict"),
"state": dict(default="present", choices=["present", "absent"]),
}
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
def main():
module = AnsibleModule(
_ARGUMENT_SPEC,
supports_check_mode=True,
)
consul_module = ConsulBindingRuleModule(module)
consul_module.execute()
if __name__ == "__main__":
main()

View file

@ -6,9 +6,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = """
module: consul_policy module: consul_policy
short_description: Manipulate Consul policies short_description: Manipulate Consul policies
version_added: 7.2.0 version_added: 7.2.0
@ -20,12 +21,17 @@ author:
- Håkon Lerring (@Hakon) - Håkon Lerring (@Hakon)
extends_documentation_fragment: extends_documentation_fragment:
- community.general.consul - community.general.consul
- community.general.consul.token
- community.general.attributes - community.general.attributes
attributes: attributes:
check_mode: check_mode:
support: none support: full
version_added: 8.3.0
diff_mode: diff_mode:
support: none support: partial
version_added: 8.3.0
details:
- In check mode the diff will miss operational attributes.
options: options:
state: state:
description: description:
@ -36,7 +42,6 @@ options:
valid_datacenters: valid_datacenters:
description: description:
- Valid datacenters for the policy. All if list is empty. - Valid datacenters for the policy. All if list is empty.
default: []
type: list type: list
elements: str elements: str
name: name:
@ -49,12 +54,11 @@ options:
description: description:
- Description of the policy. - Description of the policy.
type: str type: str
default: ''
rules: rules:
type: str type: str
description: description:
- Rule document that should be associated with the current policy. - Rule document that should be associated with the current policy.
''' """
EXAMPLES = """ EXAMPLES = """
- name: Create a policy with rules - name: Create a policy with rules
@ -95,8 +99,24 @@ EXAMPLES = """
""" """
RETURN = """ RETURN = """
policy:
description: The policy as returned by the consul HTTP API.
returned: always
type: dict
sample:
CreateIndex: 632
Description: Testing
Hash: rj5PeDHddHslkpW7Ij4OD6N4bbSXiecXFmiw2SYXg2A=
Name: foo-access
Rules: |-
key "foo" {
policy = "read"
}
key "private/foo" {
policy = "deny"
}
operation: operation:
description: The operation performed on the policy. description: The operation performed.
returned: changed returned: changed
type: str type: str
sample: update sample: update
@ -104,146 +124,39 @@ operation:
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.consul import ( from ansible_collections.community.general.plugins.module_utils.consul import (
_ConsulModule, auth_argument_spec) AUTH_ARGUMENTS_SPEC,
OPERATION_READ,
NAME_PARAMETER_NAME = "name" _ConsulModule,
DESCRIPTION_PARAMETER_NAME = "description" )
RULES_PARAMETER_NAME = "rules"
VALID_DATACENTERS_PARAMETER_NAME = "valid_datacenters"
STATE_PARAMETER_NAME = "state"
PRESENT_STATE_VALUE = "present"
ABSENT_STATE_VALUE = "absent"
REMOVE_OPERATION = "remove"
UPDATE_OPERATION = "update"
CREATE_OPERATION = "create"
_ARGUMENT_SPEC = { _ARGUMENT_SPEC = {
NAME_PARAMETER_NAME: dict(required=True), "name": dict(required=True),
DESCRIPTION_PARAMETER_NAME: dict(required=False, type='str', default=''), "description": dict(required=False, type="str"),
RULES_PARAMETER_NAME: dict(type='str'), "rules": dict(type="str"),
VALID_DATACENTERS_PARAMETER_NAME: dict(type='list', elements='str', default=[]), "valid_datacenters": dict(type="list", elements="str"),
STATE_PARAMETER_NAME: dict(default=PRESENT_STATE_VALUE, choices=[PRESENT_STATE_VALUE, ABSENT_STATE_VALUE]) "state": dict(default="present", choices=["present", "absent"]),
} }
_ARGUMENT_SPEC.update(auth_argument_spec()) _ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
def update_policy(policy, configuration, consul_module): class ConsulPolicyModule(_ConsulModule):
updated_policy = consul_module.put(('acl', 'policy', policy['ID']), data={ api_endpoint = "acl/policy"
'Name': configuration.name, # should be equal at this point. result_key = "policy"
'Description': configuration.description, unique_identifier = "id"
'Rules': configuration.rules,
'Datacenters': configuration.valid_datacenters
})
changed = ( def endpoint_url(self, operation, identifier=None):
policy.get('Rules', "") != updated_policy.get('Rules', "") or if operation == OPERATION_READ:
policy.get('Description', "") != updated_policy.get('Description', "") or return [self.api_endpoint, "name", self.params["name"]]
policy.get('Datacenters', []) != updated_policy.get('Datacenters', []) return super(ConsulPolicyModule, self).endpoint_url(operation, identifier)
)
return Output(changed=changed, operation=UPDATE_OPERATION, policy=updated_policy)
def create_policy(configuration, consul_module):
created_policy = consul_module.put('acl/policy', data={
'Name': configuration.name,
'Description': configuration.description,
'Rules': configuration.rules,
'Datacenters': configuration.valid_datacenters
})
return Output(changed=True, operation=CREATE_OPERATION, policy=created_policy)
def remove_policy(configuration, consul_module):
policies = get_policies(consul_module)
if configuration.name in policies:
policy_id = policies[configuration.name]['ID']
policy = get_policy(policy_id, consul_module)
consul_module.delete(('acl', 'policy', policy['ID']))
changed = True
else:
changed = False
return Output(changed=changed, operation=REMOVE_OPERATION)
def get_policies(consul_module):
policies = consul_module.get('acl/policies')
existing_policies_mapped_by_name = dict(
(policy['Name'], policy) for policy in policies if policy['Name'] is not None)
return existing_policies_mapped_by_name
def get_policy(id, consul_module):
return consul_module.get(('acl', 'policy', id))
def set_policy(configuration, consul_module):
policies = get_policies(consul_module)
if configuration.name in policies:
index_policy_object = policies[configuration.name]
policy_id = policies[configuration.name]['ID']
rest_policy_object = get_policy(policy_id, consul_module)
# merge dicts as some keys are only available in the partial policy
policy = index_policy_object.copy()
policy.update(rest_policy_object)
return update_policy(policy, configuration, consul_module)
else:
return create_policy(configuration, consul_module)
class Configuration:
"""
Configuration for this module.
"""
def __init__(self, name=None, description=None, rules=None, valid_datacenters=None, state=None):
self.name = name # type: str
self.description = description # type: str
self.rules = rules # type: str
self.valid_datacenters = valid_datacenters # type: str
self.state = state # type: str
class Output:
"""
Output of an action of this module.
"""
def __init__(self, changed=None, operation=None, policy=None):
self.changed = changed # type: bool
self.operation = operation # type: str
self.policy = policy # type: dict
def main(): def main():
""" module = AnsibleModule(
Main method. _ARGUMENT_SPEC,
""" supports_check_mode=True,
module = AnsibleModule(_ARGUMENT_SPEC, supports_check_mode=False)
consul_module = _ConsulModule(module)
configuration = Configuration(
name=module.params.get(NAME_PARAMETER_NAME),
description=module.params.get(DESCRIPTION_PARAMETER_NAME),
rules=module.params.get(RULES_PARAMETER_NAME),
valid_datacenters=module.params.get(VALID_DATACENTERS_PARAMETER_NAME),
state=module.params.get(STATE_PARAMETER_NAME),
) )
consul_module = ConsulPolicyModule(module)
if configuration.state == PRESENT_STATE_VALUE: consul_module.execute()
output = set_policy(configuration, consul_module)
else:
output = remove_policy(configuration, consul_module)
return_values = dict(changed=output.changed, operation=output.operation, policy=output.policy)
module.exit_json(**return_values)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -6,9 +6,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = ''' DOCUMENTATION = """
module: consul_role module: consul_role
short_description: Manipulate Consul roles short_description: Manipulate Consul roles
version_added: 7.5.0 version_added: 7.5.0
@ -20,12 +21,16 @@ author:
- Håkon Lerring (@Hakon) - Håkon Lerring (@Hakon)
extends_documentation_fragment: extends_documentation_fragment:
- community.general.consul - community.general.consul
- community.general.consul.token
- community.general.attributes - community.general.attributes
attributes: attributes:
check_mode: check_mode:
support: full support: full
diff_mode: diff_mode:
support: none support: partial
details:
- In check mode the diff will miss operational attributes.
version_added: 8.3.0
options: options:
name: name:
description: description:
@ -61,6 +66,23 @@ options:
- The ID of the policy to attach to this role; see M(community.general.consul_policy) for more info. - The ID of the policy to attach to this role; see M(community.general.consul_policy) for more info.
- Either this or O(policies[].name) must be specified. - Either this or O(policies[].name) must be specified.
type: str type: str
templated_policies:
description:
- The list of templated policies that should be applied to the role.
type: list
elements: dict
version_added: 8.3.0
suboptions:
template_name:
description:
- The templated policy name.
type: str
required: true
template_variables:
description:
- The templated policy variables.
- Not all templated policies require variables.
type: dict
service_identities: service_identities:
type: list type: list
elements: dict elements: dict
@ -69,13 +91,17 @@ options:
- If not specified, any service identities currently assigned will not be changed. - If not specified, any service identities currently assigned will not be changed.
- If the parameter is an empty array (V([])), any node identities assigned will be unassigned. - If the parameter is an empty array (V([])), any node identities assigned will be unassigned.
suboptions: suboptions:
name: service_name:
description: description:
- The name of the node. - The name of the node.
- Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character. - Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character.
- May only contain lowercase alphanumeric characters as well as - and _. - May only contain lowercase alphanumeric characters as well as - and _.
- This suboption has been renamed from O(service_identities[].name) to O(service_identities[].service_name)
in community.general 8.3.0. The old name can still be used.
type: str type: str
required: true required: true
aliases:
- name
datacenters: datacenters:
description: description:
- The datacenters the policies will be effective. - The datacenters the policies will be effective.
@ -84,7 +110,6 @@ options:
- including those which do not yet exist but may in the future. - including those which do not yet exist but may in the future.
type: list type: list
elements: str elements: str
required: true
node_identities: node_identities:
type: list type: list
elements: dict elements: dict
@ -93,20 +118,24 @@ options:
- If not specified, any node identities currently assigned will not be changed. - If not specified, any node identities currently assigned will not be changed.
- If the parameter is an empty array (V([])), any node identities assigned will be unassigned. - If the parameter is an empty array (V([])), any node identities assigned will be unassigned.
suboptions: suboptions:
name: node_name:
description: description:
- The name of the node. - The name of the node.
- Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character. - Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character.
- May only contain lowercase alphanumeric characters as well as - and _. - May only contain lowercase alphanumeric characters as well as - and _.
- This suboption has been renamed from O(node_identities[].name) to O(node_identities[].node_name)
in community.general 8.3.0. The old name can still be used.
type: str type: str
required: true required: true
aliases:
- name
datacenter: datacenter:
description: description:
- The nodes datacenter. - The nodes datacenter.
- This will result in effective policy only being valid in this datacenter. - This will result in effective policy only being valid in this datacenter.
type: str type: str
required: true required: true
''' """
EXAMPLES = """ EXAMPLES = """
- name: Create a role with 2 policies - name: Create a role with 2 policies
@ -171,373 +200,80 @@ operation:
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.consul import ( from ansible_collections.community.general.plugins.module_utils.consul import (
_ConsulModule, auth_argument_spec) AUTH_ARGUMENTS_SPEC,
OPERATION_READ,
NAME_PARAMETER_NAME = "name" _ConsulModule,
DESCRIPTION_PARAMETER_NAME = "description"
POLICIES_PARAMETER_NAME = "policies"
SERVICE_IDENTITIES_PARAMETER_NAME = "service_identities"
NODE_IDENTITIES_PARAMETER_NAME = "node_identities"
STATE_PARAMETER_NAME = "state"
PRESENT_STATE_VALUE = "present"
ABSENT_STATE_VALUE = "absent"
REMOVE_OPERATION = "remove"
UPDATE_OPERATION = "update"
CREATE_OPERATION = "create"
POLICY_RULE_SPEC = dict(
name=dict(type='str'),
id=dict(type='str'),
) )
NODE_ID_RULE_SPEC = dict(
name=dict(type='str', required=True), class ConsulRoleModule(_ConsulModule):
datacenter=dict(type='str', required=True), api_endpoint = "acl/role"
result_key = "role"
unique_identifier = "id"
def endpoint_url(self, operation, identifier=None):
if operation == OPERATION_READ:
return [self.api_endpoint, "name", self.params["name"]]
return super(ConsulRoleModule, self).endpoint_url(operation, identifier)
NAME_ID_SPEC = dict(
name=dict(type="str"),
id=dict(type="str"),
) )
SERVICE_ID_RULE_SPEC = dict( NODE_ID_SPEC = dict(
name=dict(type='str', required=True), node_name=dict(type="str", required=True, aliases=["name"]),
datacenters=dict(type='list', elements='str', required=True), datacenter=dict(type="str", required=True),
)
SERVICE_ID_SPEC = dict(
service_name=dict(type="str", required=True, aliases=["name"]),
datacenters=dict(type="list", elements="str"),
)
TEMPLATE_POLICY_SPEC = dict(
template_name=dict(type="str", required=True),
template_variables=dict(type="dict"),
) )
_ARGUMENT_SPEC = { _ARGUMENT_SPEC = {
NAME_PARAMETER_NAME: dict(required=True), "name": dict(type="str", required=True),
DESCRIPTION_PARAMETER_NAME: dict(required=False, type='str', default=None), "description": dict(type="str"),
POLICIES_PARAMETER_NAME: dict(type='list', elements='dict', options=POLICY_RULE_SPEC, "policies": dict(
mutually_exclusive=[('name', 'id')], required_one_of=[('name', 'id')], default=None), type="list",
SERVICE_IDENTITIES_PARAMETER_NAME: dict(type='list', elements='dict', options=SERVICE_ID_RULE_SPEC, default=None), elements="dict",
NODE_IDENTITIES_PARAMETER_NAME: dict(type='list', elements='dict', options=NODE_ID_RULE_SPEC, default=None), options=NAME_ID_SPEC,
STATE_PARAMETER_NAME: dict(default=PRESENT_STATE_VALUE, choices=[PRESENT_STATE_VALUE, ABSENT_STATE_VALUE]) mutually_exclusive=[("name", "id")],
required_one_of=[("name", "id")],
),
"templated_policies": dict(
type="list",
elements="dict",
options=TEMPLATE_POLICY_SPEC,
),
"node_identities": dict(
type="list",
elements="dict",
options=NODE_ID_SPEC,
),
"service_identities": dict(
type="list",
elements="dict",
options=SERVICE_ID_SPEC,
),
"state": dict(default="present", choices=["present", "absent"]),
} }
_ARGUMENT_SPEC.update(auth_argument_spec()) _ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
def compare_consul_api_role_policy_objects(first, second):
# compare two lists of dictionaries, ignoring the ID element
for x in first:
x.pop('ID', None)
for x in second:
x.pop('ID', None)
return first == second
def update_role(role, configuration, consul_module):
update_role_data = {
'Name': configuration.name,
'Description': configuration.description,
}
# check if the user omitted the description, policies, service identities, or node identities
description_specified = configuration.description is not None
policy_specified = True
if len(configuration.policies) == 1 and configuration.policies[0] is None:
policy_specified = False
service_id_specified = True
if len(configuration.service_identities) == 1 and configuration.service_identities[0] is None:
service_id_specified = False
node_id_specified = True
if len(configuration.node_identities) == 1 and configuration.node_identities[0] is None:
node_id_specified = False
if description_specified:
update_role_data["Description"] = configuration.description
if policy_specified:
update_role_data["Policies"] = [x.to_dict() for x in configuration.policies]
if configuration.version >= ConsulVersion("1.5.0") and service_id_specified:
update_role_data["ServiceIdentities"] = [
x.to_dict() for x in configuration.service_identities]
if configuration.version >= ConsulVersion("1.8.0") and node_id_specified:
update_role_data["NodeIdentities"] = [
x.to_dict() for x in configuration.node_identities]
if configuration.check_mode:
description_changed = False
if description_specified:
description_changed = role.get('Description') != update_role_data["Description"]
else:
update_role_data["Description"] = role.get("Description")
policies_changed = False
if policy_specified:
policies_changed = not (
compare_consul_api_role_policy_objects(role.get('Policies', []), update_role_data.get('Policies', [])))
else:
if role.get('Policies') is not None:
update_role_data["Policies"] = role.get('Policies')
service_ids_changed = False
if service_id_specified:
service_ids_changed = role.get('ServiceIdentities') != update_role_data.get('ServiceIdentities')
else:
if role.get('ServiceIdentities') is not None:
update_role_data["ServiceIdentities"] = role.get('ServiceIdentities')
node_ids_changed = False
if node_id_specified:
node_ids_changed = role.get('NodeIdentities') != update_role_data.get('NodeIdentities')
else:
if role.get('NodeIdentities'):
update_role_data["NodeIdentities"] = role.get('NodeIdentities')
changed = (
description_changed or
policies_changed or
service_ids_changed or
node_ids_changed
)
return Output(changed=changed, operation=UPDATE_OPERATION, role=update_role_data)
else:
# if description, policies, service or node id are not specified; we need to get the existing value and apply it
if not description_specified and role.get('Description') is not None:
update_role_data["Description"] = role.get('Description')
if not policy_specified and role.get('Policies') is not None:
update_role_data["Policies"] = role.get('Policies')
if not service_id_specified and role.get('ServiceIdentities') is not None:
update_role_data["ServiceIdentities"] = role.get('ServiceIdentities')
if not node_id_specified and role.get('NodeIdentities') is not None:
update_role_data["NodeIdentities"] = role.get('NodeIdentities')
resulting_role = consul_module.put(('acl', 'role', role['ID']), data=update_role_data)
changed = (
role['Description'] != resulting_role['Description'] or
role.get('Policies', None) != resulting_role.get('Policies', None) or
role.get('ServiceIdentities', None) != resulting_role.get('ServiceIdentities', None) or
role.get('NodeIdentities', None) != resulting_role.get('NodeIdentities', None)
)
return Output(changed=changed, operation=UPDATE_OPERATION, role=resulting_role)
def create_role(configuration, consul_module):
# check if the user omitted policies, service identities, or node identities
policy_specified = True
if len(configuration.policies) == 1 and configuration.policies[0] is None:
policy_specified = False
service_id_specified = True
if len(configuration.service_identities) == 1 and configuration.service_identities[0] is None:
service_id_specified = False
node_id_specified = True
if len(configuration.node_identities) == 1 and configuration.node_identities[0] is None:
node_id_specified = False
# get rid of None item so we can set an empty list for policies, service identities and node identities
if not policy_specified:
configuration.policies.pop()
if not service_id_specified:
configuration.service_identities.pop()
if not node_id_specified:
configuration.node_identities.pop()
create_role_data = {
'Name': configuration.name,
'Description': configuration.description,
'Policies': [x.to_dict() for x in configuration.policies],
}
if configuration.version >= ConsulVersion("1.5.0"):
create_role_data["ServiceIdentities"] = [x.to_dict() for x in configuration.service_identities]
if configuration.version >= ConsulVersion("1.8.0"):
create_role_data["NodeIdentities"] = [x.to_dict() for x in configuration.node_identities]
if not configuration.check_mode:
resulting_role = consul_module.put('acl/role', data=create_role_data)
return Output(changed=True, operation=CREATE_OPERATION, role=resulting_role)
else:
return Output(changed=True, operation=CREATE_OPERATION)
def remove_role(configuration, consul_module):
roles = get_roles(consul_module)
if configuration.name in roles:
role_id = roles[configuration.name]['ID']
if not configuration.check_mode:
consul_module.delete(('acl', 'role', role_id))
changed = True
else:
changed = False
return Output(changed=changed, operation=REMOVE_OPERATION)
def get_roles(consul_module):
roles = consul_module.get('acl/roles')
existing_roles_mapped_by_id = dict((role['Name'], role) for role in roles if role['Name'] is not None)
return existing_roles_mapped_by_id
def get_consul_version(consul_module):
config = consul_module.get('agent/self')["Config"]
return ConsulVersion(config["Version"])
def set_role(configuration, consul_module):
roles = get_roles(consul_module)
if configuration.name in roles:
role = roles[configuration.name]
return update_role(role, configuration, consul_module)
else:
return create_role(configuration, consul_module)
class ConsulVersion:
def __init__(self, version_string):
split = version_string.split('.')
self.major = split[0]
self.minor = split[1]
self.patch = split[2]
def __ge__(self, other):
return int(self.major + self.minor +
self.patch) >= int(other.major + other.minor + other.patch)
def __le__(self, other):
return int(self.major + self.minor +
self.patch) <= int(other.major + other.minor + other.patch)
class ServiceIdentity:
def __init__(self, input):
if not isinstance(input, dict) or 'name' not in input:
raise ValueError(
"Each element of service_identities must be a dict with the keys name and optionally datacenters")
self.name = input["name"]
self.datacenters = input["datacenters"] if "datacenters" in input else None
def to_dict(self):
return {
"ServiceName": self.name,
"Datacenters": self.datacenters
}
class NodeIdentity:
def __init__(self, input):
if not isinstance(input, dict) or 'name' not in input:
raise ValueError(
"Each element of node_identities must be a dict with the keys name and optionally datacenter")
self.name = input["name"]
self.datacenter = input["datacenter"] if "datacenter" in input else None
def to_dict(self):
return {
"NodeName": self.name,
"Datacenter": self.datacenter
}
class RoleLink:
def __init__(self, dict):
self.id = dict.get("id", None)
self.name = dict.get("name", None)
def to_dict(self):
return {
"ID": self.id,
"Name": self.name
}
class PolicyLink:
def __init__(self, dict):
self.id = dict.get("id", None)
self.name = dict.get("name", None)
def to_dict(self):
return {
"ID": self.id,
"Name": self.name
}
class Configuration:
"""
Configuration for this module.
"""
def __init__(self, name=None, description=None, policies=None, service_identities=None,
node_identities=None, state=None, check_mode=None):
self.name = name # type: str
self.description = description # type: str
if policies is not None:
self.policies = [PolicyLink(p) for p in policies] # type: list(PolicyLink)
else:
self.policies = [None]
if service_identities is not None:
self.service_identities = [ServiceIdentity(s) for s in service_identities] # type: list(ServiceIdentity)
else:
self.service_identities = [None]
if node_identities is not None:
self.node_identities = [NodeIdentity(n) for n in node_identities] # type: list(NodeIdentity)
else:
self.node_identities = [None]
self.state = state # type: str
self.check_mode = check_mode # type: bool
class Output:
"""
Output of an action of this module.
"""
def __init__(self, changed=None, operation=None, role=None):
self.changed = changed # type: bool
self.operation = operation # type: str
self.role = role # type: dict
def main(): def main():
""" module = AnsibleModule(
Main method. _ARGUMENT_SPEC,
""" supports_check_mode=True,
module = AnsibleModule(_ARGUMENT_SPEC, supports_check_mode=True) )
consul_module = _ConsulModule(module) consul_module = ConsulRoleModule(module)
consul_module.execute()
try:
configuration = Configuration(
name=module.params.get(NAME_PARAMETER_NAME),
description=module.params.get(DESCRIPTION_PARAMETER_NAME),
policies=module.params.get(POLICIES_PARAMETER_NAME),
service_identities=module.params.get(SERVICE_IDENTITIES_PARAMETER_NAME),
node_identities=module.params.get(NODE_IDENTITIES_PARAMETER_NAME),
state=module.params.get(STATE_PARAMETER_NAME),
check_mode=module.check_mode,
)
except ValueError as err:
module.fail_json(msg='Configuration error: %s' % str(err))
return
version = get_consul_version(consul_module)
configuration.version = version
if configuration.state == PRESENT_STATE_VALUE:
output = set_role(configuration, consul_module)
else:
output = remove_role(configuration, consul_module)
return_values = dict(changed=output.changed, operation=output.operation, role=output.role)
module.exit_json(**return_values)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -21,6 +21,7 @@ author:
- Håkon Lerring (@Hakon) - Håkon Lerring (@Hakon)
extends_documentation_fragment: extends_documentation_fragment:
- community.general.consul - community.general.consul
- community.general.consul.token
- community.general.attributes - community.general.attributes
attributes: attributes:
check_mode: check_mode:
@ -124,7 +125,7 @@ EXAMPLES = '''
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.consul import ( from ansible_collections.community.general.plugins.module_utils.consul import (
auth_argument_spec, _ConsulModule AUTH_ARGUMENTS_SPEC, _ConsulModule
) )
@ -281,7 +282,7 @@ def main():
'node', 'node',
'present']), 'present']),
datacenter=dict(type='str'), datacenter=dict(type='str'),
**auth_argument_spec() **AUTH_ARGUMENTS_SPEC
) )
module = AnsibleModule( module = AnsibleModule(

View file

@ -0,0 +1,324 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# 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
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = """
module: consul_token
short_description: Manipulate Consul tokens
version_added: 8.3.0
description:
- Allows the addition, modification and deletion of tokens in a consul
cluster via the agent. For more details on using and configuring ACLs,
see U(https://www.consul.io/docs/guides/acl.html).
author:
- Florian Apolloner (@apollo13)
extends_documentation_fragment:
- community.general.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 token should be present or absent.
choices: ['present', 'absent']
default: present
type: str
accessor_id:
description:
- Specifies a UUID to use as the token's Accessor ID.
If not specified a UUID will be generated for this field.
type: str
secret_id:
description:
- Specifies a UUID to use as the token's Secret ID.
If not specified a UUID will be generated for this field.
type: str
description:
description:
- Free form human readable description of the token.
type: str
policies:
type: list
elements: dict
description:
- List of policies to attach to the token. Each policy is a dict.
- If the parameter is left blank, any policies currently assigned will not be changed.
- Any empty array (V([])) will clear any policies previously set.
suboptions:
name:
description:
- The name of the policy to attach to this token; see M(community.general.consul_policy) for more info.
- Either this or O(policies[].id) must be specified.
type: str
id:
description:
- The ID of the policy to attach to this token; see M(community.general.consul_policy) for more info.
- Either this or O(policies[].name) must be specified.
type: str
roles:
type: list
elements: dict
description:
- List of roles to attach to the token. Each role is a dict.
- If the parameter is left blank, any roles currently assigned will not be changed.
- Any empty array (V([])) will clear any roles previously set.
suboptions:
name:
description:
- The name of the role to attach to this token; see M(community.general.consul_role) for more info.
- Either this or O(roles[].id) must be specified.
type: str
id:
description:
- The ID of the role to attach to this token; see M(community.general.consul_role) for more info.
- Either this or O(roles[].name) must be specified.
type: str
templated_policies:
description:
- The list of templated policies that should be applied to the role.
type: list
elements: dict
suboptions:
template_name:
description:
- The templated policy name.
type: str
required: true
template_variables:
description:
- The templated policy variables.
- Not all templated policies require variables.
type: dict
service_identities:
type: list
elements: dict
description:
- List of service identities to attach to the token.
- If not specified, any service identities currently assigned will not be changed.
- If the parameter is an empty array (V([])), any node identities assigned will be unassigned.
suboptions:
service_name:
description:
- The name of the service.
- Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character.
- May only contain lowercase alphanumeric characters as well as V(-) and V(_).
type: str
required: true
datacenters:
description:
- The datacenters the token will be effective.
- If an empty array (V([])) is specified, the token will valid in all datacenters.
- including those which do not yet exist but may in the future.
type: list
elements: str
node_identities:
type: list
elements: dict
description:
- List of node identities to attach to the token.
- If not specified, any node identities currently assigned will not be changed.
- If the parameter is an empty array (V([])), any node identities assigned will be unassigned.
suboptions:
node_name:
description:
- The name of the node.
- Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character.
- May only contain lowercase alphanumeric characters as well as V(-) and V(_).
type: str
required: true
datacenter:
description:
- The nodes datacenter.
- This will result in effective token only being valid in this datacenter.
type: str
required: true
local:
description:
- If true, indicates that the token should not be replicated globally
and instead be local to the current datacenter.
type: bool
expiration_ttl:
description:
- This is a convenience field and if set will initialize the C(expiration_time).
Can be specified in the form of V(60s) or V(5m) (that is, 60 seconds or 5 minutes,
respectively). Ingored when the token is updated!
type: str
"""
EXAMPLES = """
- name: Create / Update a token by accessor_id
community.general.consul_token:
state: present
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
token: 8adddd91-0bd6-d41d-ae1a-3b49cfa9a0e8
roles:
- name: role1
- name: role2
service_identities:
- service_name: service1
datacenters: [dc1, dc2]
node_identities:
- node_name: node1
datacenter: dc1
expiration_ttl: 50m
- name: Delete a token
community.general.consul_token:
state: absent
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
token: 8adddd91-0bd6-d41d-ae1a-3b49cfa9a0e8
"""
RETURN = """
token:
description: The token as returned by the consul HTTP API.
returned: always
type: dict
sample:
AccessorID: 07a7de84-c9c7-448a-99cc-beaf682efd21
CreateIndex: 632
CreateTime: "2024-01-14T21:53:01.402749174+01:00"
Description: Testing
Hash: rj5PeDHddHslkpW7Ij4OD6N4bbSXiecXFmiw2SYXg2A=
Local: false
ModifyIndex: 633
SecretID: bd380fba-da17-7cee-8576-8d6427c6c930
ServiceIdentities: [{"ServiceName": "test"}]
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,
_ConsulModule,
)
def normalize_link_obj(api_obj, module_obj, key):
api_objs = api_obj.get(key)
module_objs = module_obj.get(key)
if api_objs is None or module_objs is None:
return
name_to_id = {i["Name"]: i["ID"] for i in api_objs}
id_to_name = {i["ID"]: i["Name"] for i in api_objs}
for obj in module_objs:
identifier = obj.get("ID")
name = obj.get("Name)")
if identifier and not name and identifier in id_to_name:
obj["Name"] = id_to_name[identifier]
if not identifier and name and name in name_to_id:
obj["ID"] = name_to_id[name]
class ConsulTokenModule(_ConsulModule):
api_endpoint = "acl/token"
result_key = "token"
unique_identifier = "accessor_id"
create_only_fields = {"expiration_ttl"}
def needs_update(self, api_obj, module_obj):
# SecretID is usually not supplied
if "SecretID" not in module_obj and "SecretID" in api_obj:
del api_obj["SecretID"]
normalize_link_obj(api_obj, module_obj, "Roles")
normalize_link_obj(api_obj, module_obj, "Policies")
# ExpirationTTL is only supported on create, not for update
# it writes to ExpirationTime, so we need to remove that as well
if "ExpirationTTL" in module_obj:
del module_obj["ExpirationTTL"]
return super(ConsulTokenModule, self).needs_update(api_obj, module_obj)
NAME_ID_SPEC = dict(
name=dict(type="str"),
id=dict(type="str"),
)
NODE_ID_SPEC = dict(
node_name=dict(type="str", required=True),
datacenter=dict(type="str", required=True),
)
SERVICE_ID_SPEC = dict(
service_name=dict(type="str", required=True),
datacenters=dict(type="list", elements="str"),
)
TEMPLATE_POLICY_SPEC = dict(
template_name=dict(type="str", required=True),
template_variables=dict(type="dict"),
)
_ARGUMENT_SPEC = {
"description": dict(),
"accessor_id": dict(),
"secret_id": dict(no_log=True),
"roles": dict(
type="list",
elements="dict",
options=NAME_ID_SPEC,
mutually_exclusive=[("name", "id")],
required_one_of=[("name", "id")],
),
"policies": dict(
type="list",
elements="dict",
options=NAME_ID_SPEC,
mutually_exclusive=[("name", "id")],
required_one_of=[("name", "id")],
),
"templated_policies": dict(
type="list",
elements="dict",
options=TEMPLATE_POLICY_SPEC,
),
"node_identities": dict(
type="list",
elements="dict",
options=NODE_ID_SPEC,
),
"service_identities": dict(
type="list",
elements="dict",
options=SERVICE_ID_SPEC,
),
"local": dict(type="bool"),
"expiration_ttl": dict(type="str"),
"state": dict(default="present", choices=["present", "absent"]),
}
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
def main():
module = AnsibleModule(
_ARGUMENT_SPEC,
required_if=[("state", "absent", ["accessor_id"])],
supports_check_mode=True,
)
consul_module = ConsulTokenModule(module)
consul_module.execute()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,79 @@
---
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# 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 an auth method
community.general.consul_auth_method:
name: test
type: jwt
config:
jwt_validation_pubkeys:
- |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is changed
- result.auth_method.Type == 'jwt'
- result.operation == 'create'
- name: Update auth method
community.general.consul_auth_method:
name: test
max_token_ttl: 30m80s
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is changed
- result.auth_method.Type == 'jwt'
- result.operation == 'update'
- name: Update auth method (noop)
community.general.consul_auth_method:
name: test
max_token_ttl: 30m80s
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is not changed
- result.auth_method.Type == 'jwt'
- result.operation is not defined
- name: Delete auth method
community.general.consul_auth_method:
name: test
state: absent
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is changed
- result.operation == 'remove'
- name: Delete auth method (noop)
community.general.consul_auth_method:
name: test
state: absent
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is not changed
- result.operation is not defined

View file

@ -0,0 +1,78 @@
---
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# 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 an auth method
community.general.consul_auth_method:
name: test
type: jwt
config:
jwt_validation_pubkeys:
- |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----
token: "{{ consul_management_token }}"
- name: Create a binding rule
community.general.consul_binding_rule:
name: test-binding
description: my description
auth_method: test
token: "{{ consul_management_token }}"
bind_type: service
bind_name: yolo
register: result
- assert:
that:
- result is changed
- result.binding_rule.AuthMethod == 'test'
- result.binding.Description == 'test-binding: my description'
- result.operation == 'create'
- name: Update a binding rule
community.general.consul_binding_rule:
name: test-binding
auth_method: test
token: "{{ consul_management_token }}"
bind_name: yolo2
register: result
- assert:
that:
- result is changed
- result.binding.Description == 'test-binding: my description'
- result.operation == 'update'
- name: Update a binding rule (noop)
community.general.consul_binding_rule:
name: test-binding
auth_method: test
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is not changed
- result.binding.Description == 'test-binding: my description'
- result.operation is not defined
- name: Delete a binding rule
community.general.consul_binding_rule:
name: test-binding
auth_method: test
state: absent
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is changed
- result.operation == 'remove'

View file

@ -19,7 +19,9 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result['policy']['Name'] == 'foo-access' - result.policy.Name == 'foo-access'
- result.operation == 'create'
- name: Update the rules associated to a policy - name: Update the rules associated to a policy
consul_policy: consul_policy:
name: foo-access name: foo-access
@ -35,9 +37,12 @@
} }
token: "{{ consul_management_token }}" token: "{{ consul_management_token }}"
register: result register: result
- assert: - assert:
that: that:
- result is changed - result is changed
- result.operation == 'update'
- name: Update reports not changed when updating again without changes - name: Update reports not changed when updating again without changes
consul_policy: consul_policy:
name: foo-access name: foo-access
@ -53,9 +58,12 @@
} }
token: "{{ consul_management_token }}" token: "{{ consul_management_token }}"
register: result register: result
- assert: - assert:
that: that:
- result is not changed - result is not changed
- result.operation is not defined
- name: Remove a policy - name: Remove a policy
consul_policy: consul_policy:
name: foo-access name: foo-access
@ -64,4 +72,5 @@
register: result register: result
- assert: - assert:
that: that:
- result is changed - result is changed
- result.operation == 'remove'

View file

@ -40,7 +40,8 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result['role']['Name'] == 'foo-role-with-policy' - result.role.Name == 'foo-role-with-policy'
- result.operation == 'create'
- name: Update policy description, in check mode - name: Update policy description, in check mode
consul_role: consul_role:
@ -53,8 +54,9 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result['role']['Description'] == "Testing updating description" - result.role.Description == "Testing updating description"
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role' - result.role.Policies.0.Name == 'foo-access-for-role'
- result.operation == 'update'
- name: Update policy to add the description - name: Update policy to add the description
consul_role: consul_role:
@ -66,8 +68,9 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result['role']['Description'] == "Role for testing policies" - result.role.Description == "Role for testing policies"
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role' - result.role.Policies.0.Name == 'foo-access-for-role'
- result.operation == 'update'
- name: Update the role with another policy, also testing leaving description blank - name: Update the role with another policy, also testing leaving description blank
consul_role: consul_role:
@ -81,9 +84,10 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role' - result.role.Policies.0.Name == 'foo-access-for-role'
- result['role']['Policies'][1]['Name'] == 'bar-access-for-role' - result.role.Policies.1.Name == 'bar-access-for-role'
- result['role']['Description'] == "Role for testing policies" - result.role.Description == "Role for testing policies"
- result.operation == 'update'
- name: Create a role with service identity - name: Create a role with service identity
consul_role: consul_role:
@ -98,8 +102,8 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web" - result.role.ServiceIdentities.0.ServiceName == "web"
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1" - result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
- name: Update the role with service identity in check mode - name: Update the role with service identity in check mode
consul_role: consul_role:
@ -115,8 +119,8 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web" - result.role.ServiceIdentities.0.ServiceName == "web"
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc2" - result.role.ServiceIdentities.0.Datacenters.0 == "dc2"
- name: Update the role with service identity to add a policy, leaving the service id unchanged - name: Update the role with service identity to add a policy, leaving the service id unchanged
consul_role: consul_role:
@ -129,9 +133,9 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web" - result.role.ServiceIdentities.0.ServiceName == "web"
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1" - result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role' - result.role.Policies.0.Name == 'foo-access-for-role'
- name: Update the role with service identity to remove the policies - name: Update the role with service identity to remove the policies
consul_role: consul_role:
@ -143,9 +147,9 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web" - result.role.ServiceIdentities.0.ServiceName == "web"
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1" - result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
- result['role']['Policies'] is not defined - result.role.Policies is not defined
- name: Update the role with service identity to remove the node identities, in check mode - name: Update the role with service identity to remove the node identities, in check mode
consul_role: consul_role:
@ -158,10 +162,10 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web" - result.role.ServiceIdentities.0.ServiceName == "web"
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1" - result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
- result['role']['Policies'] is not defined - result.role.Policies is not defined
- result['role']['NodeIdentities'] == [] # in check mode the cleared field is returned as an empty array - result.role.NodeIdentities == [] # in check mode the cleared field is returned as an empty array
- name: Update the role with service identity to remove the service identities - name: Update the role with service identity to remove the service identities
consul_role: consul_role:
@ -173,8 +177,8 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result['role']['ServiceIdentities'] is not defined # in normal mode the dictionary is removed from the result - result.role.ServiceIdentities is not defined # in normal mode the dictionary is removed from the result
- result['role']['Policies'] is not defined - result.role.Policies is not defined
- name: Create a role with node identity - name: Create a role with node identity
consul_role: consul_role:
@ -188,14 +192,17 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result['role']['NodeIdentities'][0]['NodeName'] == "node-1" - result.role.NodeIdentities.0.NodeName == "node-1"
- result['role']['NodeIdentities'][0]['Datacenter'] == "dc2" - result.role.NodeIdentities.0.Datacenter == "dc2"
- name: Remove the last role - name: Remove the last role
consul_role: consul_role:
token: "{{ consul_management_token }}" token: "{{ consul_management_token }}"
name: role-with-node-identity name: role-with-node-identity
state: absent state: absent
register: result
- assert: - assert:
that: that:
- result is changed - result is changed
- result.operation == 'remove'

View file

@ -0,0 +1,82 @@
---
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# 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 policy with rules
community.general.consul_policy:
name: "{{ item }}"
rules: |
key "foo" {
policy = "read"
}
token: "{{ consul_management_token }}"
loop:
- foo-access
- foo-access2
- name: Create token
community.general.consul_token:
state: present
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
token: "{{ consul_management_token }}"
service_identities:
- service_name: test
datacenters: [test1, test2]
node_identities:
- node_name: test
datacenter: test
policies:
- name: foo-access
- name: foo-access2
expiration_ttl: 1h
register: create_result
- assert:
that:
- create_result is changed
- create_result.token.AccessorID == "07a7de84-c9c7-448a-99cc-beaf682efd21"
- create_result.operation == 'create'
- name: Update token
community.general.consul_token:
state: present
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
token: "{{ consul_management_token }}"
description: Testing
policies:
- id: "{{ create_result.token.Policies[-1].ID }}"
service_identities: []
register: result
- assert:
that:
- result is changed
- result.operation == 'update'
- name: Update token (noop)
community.general.consul_token:
state: present
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
token: "{{ consul_management_token }}"
policies:
- id: "{{ create_result.token.Policies[-1].ID }}"
register: result
- assert:
that:
- result is not changed
- result.operation is not defined
- name: Remove token
community.general.consul_token:
state: absent
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is changed
- not result.token
- result.operation == 'remove'

View file

@ -77,12 +77,10 @@
- name: Start Consul (dev mode enabled) - name: Start Consul (dev mode enabled)
shell: nohup {{ consul_cmd }} agent -dev -config-file {{ remote_tmp_dir }}/consul_config.hcl </dev/null >/dev/null 2>&1 & shell: nohup {{ consul_cmd }} agent -dev -config-file {{ remote_tmp_dir }}/consul_config.hcl </dev/null >/dev/null 2>&1 &
- name: Bootstrap ACL - name: Bootstrap ACL
command: '{{ consul_cmd }} acl bootstrap --format=json' consul_acl_bootstrap:
register: consul_bootstrap_result_string register: consul_bootstrap_result
- set_fact: - set_fact:
consul_management_token: '{{ consul_bootstrap_json_result["SecretID"] }}' consul_management_token: '{{ consul_bootstrap_result.result.SecretID }}'
vars:
consul_bootstrap_json_result: '{{ consul_bootstrap_result_string.stdout | from_json }}'
- name: Create some data - name: Create some data
command: '{{ consul_cmd }} kv put -token={{consul_management_token}} data/value{{ item }} foo{{ item }}' command: '{{ consul_cmd }} kv put -token={{consul_management_token}} data/value{{ item }} foo{{ item }}'
loop: loop:
@ -94,6 +92,9 @@
- import_tasks: consul_session.yml - import_tasks: consul_session.yml
- import_tasks: consul_policy.yml - import_tasks: consul_policy.yml
- import_tasks: consul_role.yml - import_tasks: consul_role.yml
- import_tasks: consul_token.yml
- import_tasks: consul_auth_method.yml
- import_tasks: consul_binding_rule.yml
always: always:
- name: Kill consul process - name: Kill consul process
shell: kill $(cat {{ remote_tmp_dir }}/consul.pid) shell: kill $(cat {{ remote_tmp_dir }}/consul.pid)