mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
[PR #7878/29f98654 backport][stable-8] Add new consul modules and reuse code between them. (#7902)
Add new consul modules and reuse code between them. (#7878)
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:
parent
1ee2bba140
commit
0a904d60cd
17 changed files with 1508 additions and 568 deletions
2
.github/BOTMETA.yml
vendored
2
.github/BOTMETA.yml
vendored
|
@ -1494,7 +1494,7 @@ macros:
|
|||
team_ansible_core:
|
||||
team_aix: MorrisA bcoca d-little flynn1973 gforster kairoaraujo marvin-sinister mator molekuul ramooncamacho wtcross
|
||||
team_bsd: JoergFiedler MacLemon bcoca dch jasperla mekanix opoplawski overhacked tuxillo
|
||||
team_consul: sgargan
|
||||
team_consul: sgargan apollo13
|
||||
team_cyberark_conjur: jvanderhoof ryanprior
|
||||
team_e_spirit: MatrixCrawler getjack
|
||||
team_flatpak: JayKayy oolongbrothers
|
||||
|
|
|
@ -1,2 +1,7 @@
|
|||
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).
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
|
@ -33,12 +34,16 @@ options:
|
|||
description:
|
||||
- Whether to verify the TLS certificate of the consul agent.
|
||||
default: true
|
||||
token:
|
||||
description:
|
||||
- The token to use for authorization.
|
||||
type: str
|
||||
ca_path:
|
||||
description:
|
||||
- The CA bundle to use for https connections
|
||||
type: str
|
||||
"""
|
||||
|
||||
TOKEN = r"""
|
||||
options:
|
||||
token:
|
||||
description:
|
||||
- The token to use for authorization.
|
||||
type: str
|
||||
"""
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
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):
|
||||
return '%s://%s:%s/v1' % (configuration.scheme,
|
||||
configuration.host, configuration.port)
|
||||
return "%s://%s:%s/v1" % (
|
||||
configuration.scheme,
|
||||
configuration.host,
|
||||
configuration.port,
|
||||
)
|
||||
|
||||
|
||||
def get_auth_headers(configuration):
|
||||
if configuration.token is None:
|
||||
return {}
|
||||
else:
|
||||
return {'X-Consul-Token': configuration.token}
|
||||
return {"X-Consul-Token": configuration.token}
|
||||
|
||||
|
||||
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):
|
||||
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():
|
||||
return dict(
|
||||
host=dict(default="localhost"),
|
||||
port=dict(type="int", default=8500),
|
||||
scheme=dict(default="http"),
|
||||
validate_certs=dict(type="bool", default=True),
|
||||
token=dict(no_log=True),
|
||||
ca_path=dict(),
|
||||
)
|
||||
AUTH_ARGUMENTS_SPEC = dict(
|
||||
host=dict(default="localhost"),
|
||||
port=dict(type="int", default=8500),
|
||||
scheme=dict(default="http"),
|
||||
validate_certs=dict(type="bool", default=True),
|
||||
token=dict(no_log=True),
|
||||
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:
|
||||
|
@ -53,13 +104,160 @@ class _ConsulModule:
|
|||
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):
|
||||
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):
|
||||
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]
|
||||
if params:
|
||||
# Remove values that are None
|
||||
|
@ -74,7 +272,7 @@ class _ConsulModule:
|
|||
url = "/".join([base_url] + list(url_parts))
|
||||
|
||||
headers = {}
|
||||
token = self.module.params.get("token")
|
||||
token = self.params.get("token")
|
||||
if token:
|
||||
headers["X-Consul-Token"] = token
|
||||
|
||||
|
@ -93,19 +291,25 @@ class _ConsulModule:
|
|||
ca_path=ca_path,
|
||||
)
|
||||
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 = (
|
||||
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):
|
||||
return self._request("GET", url_parts, **kwargs)
|
||||
|
|
108
plugins/modules/consul_acl_bootstrap.py
Normal file
108
plugins/modules/consul_acl_bootstrap.py
Normal 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()
|
206
plugins/modules/consul_auth_method.py
Normal file
206
plugins/modules/consul_auth_method.py
Normal 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()
|
182
plugins/modules/consul_binding_rule.py
Normal file
182
plugins/modules/consul_binding_rule.py
Normal 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()
|
|
@ -6,9 +6,10 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
DOCUMENTATION = """
|
||||
module: consul_policy
|
||||
short_description: Manipulate Consul policies
|
||||
version_added: 7.2.0
|
||||
|
@ -20,12 +21,17 @@ author:
|
|||
- Håkon Lerring (@Hakon)
|
||||
extends_documentation_fragment:
|
||||
- community.general.consul
|
||||
- community.general.consul.token
|
||||
- community.general.attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
support: none
|
||||
support: full
|
||||
version_added: 8.3.0
|
||||
diff_mode:
|
||||
support: none
|
||||
support: partial
|
||||
version_added: 8.3.0
|
||||
details:
|
||||
- In check mode the diff will miss operational attributes.
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
|
@ -36,7 +42,6 @@ options:
|
|||
valid_datacenters:
|
||||
description:
|
||||
- Valid datacenters for the policy. All if list is empty.
|
||||
default: []
|
||||
type: list
|
||||
elements: str
|
||||
name:
|
||||
|
@ -49,12 +54,11 @@ options:
|
|||
description:
|
||||
- Description of the policy.
|
||||
type: str
|
||||
default: ''
|
||||
rules:
|
||||
type: str
|
||||
description:
|
||||
- Rule document that should be associated with the current policy.
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create a policy with rules
|
||||
|
@ -95,8 +99,24 @@ EXAMPLES = """
|
|||
"""
|
||||
|
||||
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:
|
||||
description: The operation performed on the policy.
|
||||
description: The operation performed.
|
||||
returned: changed
|
||||
type: str
|
||||
sample: update
|
||||
|
@ -104,146 +124,39 @@ operation:
|
|||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.consul import (
|
||||
_ConsulModule, auth_argument_spec)
|
||||
|
||||
NAME_PARAMETER_NAME = "name"
|
||||
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"
|
||||
AUTH_ARGUMENTS_SPEC,
|
||||
OPERATION_READ,
|
||||
_ConsulModule,
|
||||
)
|
||||
|
||||
_ARGUMENT_SPEC = {
|
||||
NAME_PARAMETER_NAME: dict(required=True),
|
||||
DESCRIPTION_PARAMETER_NAME: dict(required=False, type='str', default=''),
|
||||
RULES_PARAMETER_NAME: dict(type='str'),
|
||||
VALID_DATACENTERS_PARAMETER_NAME: dict(type='list', elements='str', default=[]),
|
||||
STATE_PARAMETER_NAME: dict(default=PRESENT_STATE_VALUE, choices=[PRESENT_STATE_VALUE, ABSENT_STATE_VALUE])
|
||||
"name": dict(required=True),
|
||||
"description": dict(required=False, type="str"),
|
||||
"rules": dict(type="str"),
|
||||
"valid_datacenters": dict(type="list", elements="str"),
|
||||
"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):
|
||||
updated_policy = consul_module.put(('acl', 'policy', policy['ID']), data={
|
||||
'Name': configuration.name, # should be equal at this point.
|
||||
'Description': configuration.description,
|
||||
'Rules': configuration.rules,
|
||||
'Datacenters': configuration.valid_datacenters
|
||||
})
|
||||
class ConsulPolicyModule(_ConsulModule):
|
||||
api_endpoint = "acl/policy"
|
||||
result_key = "policy"
|
||||
unique_identifier = "id"
|
||||
|
||||
changed = (
|
||||
policy.get('Rules', "") != updated_policy.get('Rules', "") or
|
||||
policy.get('Description', "") != updated_policy.get('Description', "") or
|
||||
policy.get('Datacenters', []) != updated_policy.get('Datacenters', [])
|
||||
)
|
||||
|
||||
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 endpoint_url(self, operation, identifier=None):
|
||||
if operation == OPERATION_READ:
|
||||
return [self.api_endpoint, "name", self.params["name"]]
|
||||
return super(ConsulPolicyModule, self).endpoint_url(operation, identifier)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main method.
|
||||
"""
|
||||
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),
|
||||
module = AnsibleModule(
|
||||
_ARGUMENT_SPEC,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
||||
if configuration.state == PRESENT_STATE_VALUE:
|
||||
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)
|
||||
consul_module = ConsulPolicyModule(module)
|
||||
consul_module.execute()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = '''
|
||||
DOCUMENTATION = """
|
||||
module: consul_role
|
||||
short_description: Manipulate Consul roles
|
||||
version_added: 7.5.0
|
||||
|
@ -20,12 +21,16 @@ author:
|
|||
- Håkon Lerring (@Hakon)
|
||||
extends_documentation_fragment:
|
||||
- community.general.consul
|
||||
- community.general.consul.token
|
||||
- community.general.attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
support: full
|
||||
diff_mode:
|
||||
support: none
|
||||
support: partial
|
||||
details:
|
||||
- In check mode the diff will miss operational attributes.
|
||||
version_added: 8.3.0
|
||||
options:
|
||||
name:
|
||||
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.
|
||||
- Either this or O(policies[].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
|
||||
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:
|
||||
type: list
|
||||
elements: dict
|
||||
|
@ -69,13 +91,17 @@ options:
|
|||
- 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:
|
||||
name:
|
||||
service_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 - 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
|
||||
required: true
|
||||
aliases:
|
||||
- name
|
||||
datacenters:
|
||||
description:
|
||||
- The datacenters the policies will be effective.
|
||||
|
@ -84,7 +110,6 @@ options:
|
|||
- including those which do not yet exist but may in the future.
|
||||
type: list
|
||||
elements: str
|
||||
required: true
|
||||
node_identities:
|
||||
type: list
|
||||
elements: dict
|
||||
|
@ -93,20 +118,24 @@ options:
|
|||
- 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:
|
||||
name:
|
||||
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 - 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
|
||||
required: true
|
||||
aliases:
|
||||
- name
|
||||
datacenter:
|
||||
description:
|
||||
- The nodes datacenter.
|
||||
- This will result in effective policy only being valid in this datacenter.
|
||||
type: str
|
||||
required: true
|
||||
'''
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
- name: Create a role with 2 policies
|
||||
|
@ -171,373 +200,80 @@ operation:
|
|||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.consul import (
|
||||
_ConsulModule, auth_argument_spec)
|
||||
|
||||
NAME_PARAMETER_NAME = "name"
|
||||
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'),
|
||||
AUTH_ARGUMENTS_SPEC,
|
||||
OPERATION_READ,
|
||||
_ConsulModule,
|
||||
)
|
||||
|
||||
NODE_ID_RULE_SPEC = dict(
|
||||
name=dict(type='str', required=True),
|
||||
datacenter=dict(type='str', required=True),
|
||||
|
||||
class ConsulRoleModule(_ConsulModule):
|
||||
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(
|
||||
name=dict(type='str', required=True),
|
||||
datacenters=dict(type='list', elements='str', required=True),
|
||||
NODE_ID_SPEC = dict(
|
||||
node_name=dict(type="str", required=True, aliases=["name"]),
|
||||
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 = {
|
||||
NAME_PARAMETER_NAME: dict(required=True),
|
||||
DESCRIPTION_PARAMETER_NAME: dict(required=False, type='str', default=None),
|
||||
POLICIES_PARAMETER_NAME: dict(type='list', elements='dict', options=POLICY_RULE_SPEC,
|
||||
mutually_exclusive=[('name', 'id')], required_one_of=[('name', 'id')], default=None),
|
||||
SERVICE_IDENTITIES_PARAMETER_NAME: dict(type='list', elements='dict', options=SERVICE_ID_RULE_SPEC, default=None),
|
||||
NODE_IDENTITIES_PARAMETER_NAME: dict(type='list', elements='dict', options=NODE_ID_RULE_SPEC, default=None),
|
||||
STATE_PARAMETER_NAME: dict(default=PRESENT_STATE_VALUE, choices=[PRESENT_STATE_VALUE, ABSENT_STATE_VALUE])
|
||||
"name": dict(type="str", required=True),
|
||||
"description": dict(type="str"),
|
||||
"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,
|
||||
),
|
||||
"state": dict(default="present", choices=["present", "absent"]),
|
||||
}
|
||||
_ARGUMENT_SPEC.update(auth_argument_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
|
||||
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main method.
|
||||
"""
|
||||
module = AnsibleModule(_ARGUMENT_SPEC, supports_check_mode=True)
|
||||
consul_module = _ConsulModule(module)
|
||||
|
||||
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)
|
||||
module = AnsibleModule(
|
||||
_ARGUMENT_SPEC,
|
||||
supports_check_mode=True,
|
||||
)
|
||||
consul_module = ConsulRoleModule(module)
|
||||
consul_module.execute()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -21,6 +21,7 @@ author:
|
|||
- Håkon Lerring (@Hakon)
|
||||
extends_documentation_fragment:
|
||||
- community.general.consul
|
||||
- community.general.consul.token
|
||||
- community.general.attributes
|
||||
attributes:
|
||||
check_mode:
|
||||
|
@ -124,7 +125,7 @@ EXAMPLES = '''
|
|||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
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',
|
||||
'present']),
|
||||
datacenter=dict(type='str'),
|
||||
**auth_argument_spec()
|
||||
**AUTH_ARGUMENTS_SPEC
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
|
|
324
plugins/modules/consul_token.py
Normal file
324
plugins/modules/consul_token.py
Normal 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()
|
|
@ -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
|
|
@ -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'
|
|
@ -19,7 +19,9 @@
|
|||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['policy']['Name'] == 'foo-access'
|
||||
- result.policy.Name == 'foo-access'
|
||||
- result.operation == 'create'
|
||||
|
||||
- name: Update the rules associated to a policy
|
||||
consul_policy:
|
||||
name: foo-access
|
||||
|
@ -35,9 +37,12 @@
|
|||
}
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result.operation == 'update'
|
||||
|
||||
- name: Update reports not changed when updating again without changes
|
||||
consul_policy:
|
||||
name: foo-access
|
||||
|
@ -53,9 +58,12 @@
|
|||
}
|
||||
token: "{{ consul_management_token }}"
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is not changed
|
||||
- result.operation is not defined
|
||||
|
||||
- name: Remove a policy
|
||||
consul_policy:
|
||||
name: foo-access
|
||||
|
@ -64,4 +72,5 @@
|
|||
register: result
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result is changed
|
||||
- result.operation == 'remove'
|
|
@ -40,7 +40,8 @@
|
|||
- assert:
|
||||
that:
|
||||
- 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
|
||||
consul_role:
|
||||
|
@ -53,8 +54,9 @@
|
|||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['Description'] == "Testing updating description"
|
||||
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role'
|
||||
- result.role.Description == "Testing updating description"
|
||||
- result.role.Policies.0.Name == 'foo-access-for-role'
|
||||
- result.operation == 'update'
|
||||
|
||||
- name: Update policy to add the description
|
||||
consul_role:
|
||||
|
@ -66,8 +68,9 @@
|
|||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['Description'] == "Role for testing policies"
|
||||
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role'
|
||||
- result.role.Description == "Role for testing policies"
|
||||
- result.role.Policies.0.Name == 'foo-access-for-role'
|
||||
- result.operation == 'update'
|
||||
|
||||
- name: Update the role with another policy, also testing leaving description blank
|
||||
consul_role:
|
||||
|
@ -81,9 +84,10 @@
|
|||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role'
|
||||
- result['role']['Policies'][1]['Name'] == 'bar-access-for-role'
|
||||
- result['role']['Description'] == "Role for testing policies"
|
||||
- result.role.Policies.0.Name == 'foo-access-for-role'
|
||||
- result.role.Policies.1.Name == 'bar-access-for-role'
|
||||
- result.role.Description == "Role for testing policies"
|
||||
- result.operation == 'update'
|
||||
|
||||
- name: Create a role with service identity
|
||||
consul_role:
|
||||
|
@ -98,8 +102,8 @@
|
|||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
|
||||
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1"
|
||||
- result.role.ServiceIdentities.0.ServiceName == "web"
|
||||
- result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
|
||||
|
||||
- name: Update the role with service identity in check mode
|
||||
consul_role:
|
||||
|
@ -115,8 +119,8 @@
|
|||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
|
||||
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc2"
|
||||
- result.role.ServiceIdentities.0.ServiceName == "web"
|
||||
- result.role.ServiceIdentities.0.Datacenters.0 == "dc2"
|
||||
|
||||
- name: Update the role with service identity to add a policy, leaving the service id unchanged
|
||||
consul_role:
|
||||
|
@ -129,9 +133,9 @@
|
|||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
|
||||
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1"
|
||||
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role'
|
||||
- result.role.ServiceIdentities.0.ServiceName == "web"
|
||||
- result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
|
||||
- result.role.Policies.0.Name == 'foo-access-for-role'
|
||||
|
||||
- name: Update the role with service identity to remove the policies
|
||||
consul_role:
|
||||
|
@ -143,9 +147,9 @@
|
|||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
|
||||
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1"
|
||||
- result['role']['Policies'] is not defined
|
||||
- result.role.ServiceIdentities.0.ServiceName == "web"
|
||||
- result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
|
||||
- result.role.Policies is not defined
|
||||
|
||||
- name: Update the role with service identity to remove the node identities, in check mode
|
||||
consul_role:
|
||||
|
@ -158,10 +162,10 @@
|
|||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
|
||||
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1"
|
||||
- result['role']['Policies'] is not defined
|
||||
- result['role']['NodeIdentities'] == [] # in check mode the cleared field is returned as an empty array
|
||||
- result.role.ServiceIdentities.0.ServiceName == "web"
|
||||
- result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
|
||||
- result.role.Policies is not defined
|
||||
- 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
|
||||
consul_role:
|
||||
|
@ -173,8 +177,8 @@
|
|||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['ServiceIdentities'] is not defined # in normal mode the dictionary is removed from the result
|
||||
- result['role']['Policies'] is not defined
|
||||
- result.role.ServiceIdentities is not defined # in normal mode the dictionary is removed from the result
|
||||
- result.role.Policies is not defined
|
||||
|
||||
- name: Create a role with node identity
|
||||
consul_role:
|
||||
|
@ -188,14 +192,17 @@
|
|||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result['role']['NodeIdentities'][0]['NodeName'] == "node-1"
|
||||
- result['role']['NodeIdentities'][0]['Datacenter'] == "dc2"
|
||||
- result.role.NodeIdentities.0.NodeName == "node-1"
|
||||
- result.role.NodeIdentities.0.Datacenter == "dc2"
|
||||
|
||||
- name: Remove the last role
|
||||
consul_role:
|
||||
token: "{{ consul_management_token }}"
|
||||
name: role-with-node-identity
|
||||
state: absent
|
||||
register: result
|
||||
|
||||
- assert:
|
||||
that:
|
||||
- result is changed
|
||||
- result is changed
|
||||
- result.operation == 'remove'
|
82
tests/integration/targets/consul/tasks/consul_token.yml
Normal file
82
tests/integration/targets/consul/tasks/consul_token.yml
Normal 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'
|
|
@ -77,12 +77,10 @@
|
|||
- 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 &
|
||||
- name: Bootstrap ACL
|
||||
command: '{{ consul_cmd }} acl bootstrap --format=json'
|
||||
register: consul_bootstrap_result_string
|
||||
consul_acl_bootstrap:
|
||||
register: consul_bootstrap_result
|
||||
- set_fact:
|
||||
consul_management_token: '{{ consul_bootstrap_json_result["SecretID"] }}'
|
||||
vars:
|
||||
consul_bootstrap_json_result: '{{ consul_bootstrap_result_string.stdout | from_json }}'
|
||||
consul_management_token: '{{ consul_bootstrap_result.result.SecretID }}'
|
||||
- name: Create some data
|
||||
command: '{{ consul_cmd }} kv put -token={{consul_management_token}} data/value{{ item }} foo{{ item }}'
|
||||
loop:
|
||||
|
@ -94,6 +92,9 @@
|
|||
- import_tasks: consul_session.yml
|
||||
- import_tasks: consul_policy.yml
|
||||
- import_tasks: consul_role.yml
|
||||
- import_tasks: consul_token.yml
|
||||
- import_tasks: consul_auth_method.yml
|
||||
- import_tasks: consul_binding_rule.yml
|
||||
always:
|
||||
- name: Kill consul process
|
||||
shell: kill $(cat {{ remote_tmp_dir }}/consul.pid)
|
||||
|
|
Loading…
Reference in a new issue