From edd4637b9f2a4e54353f2db3c7678e843e17fc3a Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 17 Sep 2023 14:51:07 +0200 Subject: [PATCH] [PR #6972/d0f229f5 backport][stable-7] Add consul_role module from domant PR (#7282) Add consul_role module from domant PR (#6972) * Update as per PR comments * Move common code to module_utils * Break up long import line * Fix pipeline errors * Inital version of check_mode support * Fix updating a role, add tests * Fix line spacing * Fix line indentation * Add consul-role tests * Fixes for role update * Apply suggestions from code review Co-authored-by: Felix Fontein * Update as per MR comments * Update as per MR comments * Fix documentation issues * Add types for sub-options * Allow setting of policy, service and node id fields by specifying a value, or leaving them unchanged by omitting them * Fix typo in test * Apply suggestions from code review Co-authored-by: Felix Fontein * Reset and force push to get rid of merge * Corrected unit tests * Apply suggestions from code review Co-authored-by: Felix Fontein * Add suboptions documentation for node and service identities * Fix PEP errors from pipeline * Fix pipeline errors. * Fix more pipeline errors * Apply suggestions from code review Co-authored-by: Felix Fontein * Fix line that is too long * Not specifying a value for description during update now leaves existing value unchanged * Fixes for pipeline errors * Add test cases to verify handling description works --------- Co-authored-by: Felix Fontein (cherry picked from commit d0f229f5d8eb89f5e4d6858deb6ce90675d37908) Co-authored-by: Valerio Poggi <106782233+valeriopoggi@users.noreply.github.com> --- plugins/module_utils/consul.py | 29 + plugins/modules/consul_role.py | 644 ++++++++++++++++++ .../targets/consul/tasks/consul_role.yml | 201 ++++++ .../integration/targets/consul/tasks/main.yml | 1 + 4 files changed, 875 insertions(+) create mode 100644 plugins/module_utils/consul.py create mode 100644 plugins/modules/consul_role.py create mode 100644 tests/integration/targets/consul/tasks/consul_role.yml diff --git a/plugins/module_utils/consul.py b/plugins/module_utils/consul.py new file mode 100644 index 0000000000..1e820b7fe0 --- /dev/null +++ b/plugins/module_utils/consul.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Håkon Lerring +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +def get_consul_url(configuration): + 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} + + +class RequestError(Exception): + pass + + +def handle_consul_response_error(response): + if 400 <= response.status_code < 600: + raise RequestError('%d %s' % (response.status_code, response.content)) diff --git a/plugins/modules/consul_role.py b/plugins/modules/consul_role.py new file mode 100644 index 0000000000..e41ff35464 --- /dev/null +++ b/plugins/modules/consul_role.py @@ -0,0 +1,644 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Håkon Lerring +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +module: consul_role +short_description: Manipulate Consul roles +version_added: 7.5.0 +description: + - Allows the addition, modification and deletion of roles 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: + - Håkon Lerring (@Hakon) +extends_documentation_fragment: + - community.general.attributes +attributes: + check_mode: + support: full + diff_mode: + support: none +options: + name: + description: + - A name used to identify the role. + required: true + type: str + state: + description: + - whether the role should be present or absent. + required: false + choices: ['present', 'absent'] + default: present + type: str + description: + description: + - Description of the role. + - If not specified, the assigned description will not be changed. + required: false + type: str + policies: + type: list + elements: dict + description: + - List of policies to attach to the role. 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. + required: false + suboptions: + name: + description: + - The name of the policy to attach to this role; 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 role; see M(community.general.consul_policy) for more info. + - Either this or O(policies[].name) must be specified. + type: str + service_identities: + type: list + elements: dict + description: + - List of service identities to attach to the role. + - 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. + required: false + suboptions: + 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 _. + type: str + required: true + datacenters: + description: + - The datacenters the policies will be effective. + - This will result in effective policy only being valid in this datacenter. + - If an empty array (V([])) is specified, the policies will valid in all datacenters. + - including those which do not yet exist but may in the future. + type: list + elements: str + required: true + node_identities: + type: list + elements: dict + description: + - List of node identities to attach to the role. + - 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. + required: false + suboptions: + 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 _. + type: str + required: true + datacenter: + description: + - The nodes datacenter. + - This will result in effective policy only being valid in this datacenter. + type: str + required: true + host: + description: + - Host of the consul agent, defaults to V(localhost). + required: false + default: localhost + type: str + port: + type: int + description: + - The port on which the consul agent is running. + required: false + default: 8500 + scheme: + description: + - The protocol scheme on which the consul agent is running. + required: false + default: http + type: str + token: + description: + - A management token is required to manipulate the roles. + type: str + validate_certs: + type: bool + description: + - Whether to verify the TLS certificate of the consul agent. + required: false + default: true +requirements: + - requests +''' + +EXAMPLES = """ +- name: Create a role with 2 policies + community.general.consul_role: + host: consul1.example.com + token: some_management_acl + name: foo-role + policies: + - id: 783beef3-783f-f41f-7422-7087dc272765 + - name: "policy-1" + +- name: Create a role with service identity + community.general.consul_role: + host: consul1.example.com + token: some_management_acl + name: foo-role-2 + service_identities: + - name: web + datacenters: + - dc1 + +- name: Create a role with node identity + community.general.consul_role: + host: consul1.example.com + token: some_management_acl + name: foo-role-3 + node_identities: + - name: node-1 + datacenter: dc2 + +- name: Remove a role + community.general.consul_role: + host: consul1.example.com + token: some_management_acl + name: foo-role-3 + state: absent +""" + +RETURN = """ +role: + description: The role object. + returned: success + type: dict + sample: + { + "CreateIndex": 39, + "Description": "", + "Hash": "Trt0QJtxVEfvTTIcdTUbIJRr6Dsi6E4EcwSFxx9tCYM=", + "ID": "9a300b8d-48db-b720-8544-a37c0f5dafb5", + "ModifyIndex": 39, + "Name": "foo-role", + "Policies": [ + {"ID": "b1a00172-d7a1-0e66-a12e-7a4045c4b774", "Name": "foo-access"} + ] + } +operation: + description: The operation performed on the role. + returned: changed + type: str + sample: update +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import missing_required_lib +from ansible_collections.community.general.plugins.module_utils.consul import ( + get_consul_url, get_auth_headers, handle_consul_response_error) +import traceback + +REQUESTS_IMP_ERR = None + +try: + from requests.exceptions import ConnectionError + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + REQUESTS_IMP_ERR = traceback.format_exc() + +TOKEN_PARAMETER_NAME = "token" +HOST_PARAMETER_NAME = "host" +SCHEME_PARAMETER_NAME = "scheme" +VALIDATE_CERTS_PARAMETER_NAME = "validate_certs" +NAME_PARAMETER_NAME = "name" +DESCRIPTION_PARAMETER_NAME = "description" +PORT_PARAMETER_NAME = "port" +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), + datacenter=dict(type='str', required=True), +) + +SERVICE_ID_RULE_SPEC = dict( + name=dict(type='str', required=True), + datacenters=dict(type='list', elements='str', required=True), +) + +_ARGUMENT_SPEC = { + TOKEN_PARAMETER_NAME: dict(no_log=True), + PORT_PARAMETER_NAME: dict(default=8500, type='int'), + HOST_PARAMETER_NAME: dict(default='localhost'), + SCHEME_PARAMETER_NAME: dict(default='http'), + VALIDATE_CERTS_PARAMETER_NAME: dict(type='bool', default=True), + 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]), +} + + +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): + url = '%s/acl/role/%s' % (get_consul_url(configuration), + role['ID']) + headers = get_auth_headers(configuration) + + 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') + + response = requests.put(url, headers=headers, json=update_role_data, verify=configuration.validate_certs) + handle_consul_response_error(response) + + resulting_role = response.json() + 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): + url = '%s/acl/role' % get_consul_url(configuration) + headers = get_auth_headers(configuration) + + # 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 emtpy 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: + response = requests.put(url, headers=headers, json=create_role_data, verify=configuration.validate_certs) + handle_consul_response_error(response) + + resulting_role = response.json() + + return Output(changed=True, operation=CREATE_OPERATION, role=resulting_role) + else: + return Output(changed=True, operation=CREATE_OPERATION) + + +def remove_role(configuration): + roles = get_roles(configuration) + + if configuration.name in roles: + + role_id = roles[configuration.name]['ID'] + + if not configuration.check_mode: + url = '%s/acl/role/%s' % (get_consul_url(configuration), role_id) + headers = get_auth_headers(configuration) + response = requests.delete(url, headers=headers, verify=configuration.validate_certs) + handle_consul_response_error(response) + + changed = True + else: + changed = False + return Output(changed=changed, operation=REMOVE_OPERATION) + + +def get_roles(configuration): + url = '%s/acl/roles' % get_consul_url(configuration) + headers = get_auth_headers(configuration) + response = requests.get(url, headers=headers, verify=configuration.validate_certs) + handle_consul_response_error(response) + roles = response.json() + 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(configuration): + url = '%s/agent/self' % get_consul_url(configuration) + headers = get_auth_headers(configuration) + response = requests.get(url, headers=headers, verify=configuration.validate_certs) + handle_consul_response_error(response) + config = response.json()["Config"] + return ConsulVersion(config["Version"]) + + +def set_role(configuration): + roles = get_roles(configuration) + + if configuration.name in roles: + role = roles[configuration.name] + return update_role(role, configuration) + else: + return create_role(configuration) + + +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, token=None, host=None, scheme=None, validate_certs=None, name=None, description=None, port=None, + policies=None, service_identities=None, node_identities=None, state=None, check_mode=None): + self.token = token # type: str + self.host = host # type: str + self.port = port # type: int + self.scheme = scheme # type: str + self.validate_certs = validate_certs # type: bool + 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(): + """ + Main method. + """ + module = AnsibleModule(_ARGUMENT_SPEC, supports_check_mode=True) + + if not HAS_REQUESTS: + module.fail_json(msg=missing_required_lib("requests"), + exception=REQUESTS_IMP_ERR) + + try: + configuration = Configuration( + token=module.params.get(TOKEN_PARAMETER_NAME), + host=module.params.get(HOST_PARAMETER_NAME), + port=module.params.get(PORT_PARAMETER_NAME), + scheme=module.params.get(SCHEME_PARAMETER_NAME), + validate_certs=module.params.get(VALIDATE_CERTS_PARAMETER_NAME), + 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 + + try: + + version = get_consul_version(configuration) + configuration.version = version + + if configuration.state == PRESENT_STATE_VALUE: + output = set_role(configuration) + else: + output = remove_role(configuration) + except ConnectionError as e: + module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % ( + configuration.host, configuration.port, str(e))) + raise + + return_values = dict(changed=output.changed, operation=output.operation, role=output.role) + module.exit_json(**return_values) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/consul/tasks/consul_role.yml b/tests/integration/targets/consul/tasks/consul_role.yml new file mode 100644 index 0000000000..cd83929ef9 --- /dev/null +++ b/tests/integration/targets/consul/tasks/consul_role.yml @@ -0,0 +1,201 @@ +--- +# Copyright (c) Ansible Project +# 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 + consul_policy: + name: foo-access-for-role + rules: | + key "foo" { + policy = "read" + } + key "private/foo" { + policy = "deny" + } + token: "{{ consul_management_token }}" + register: policy_result + +- name: Create another policy with rules + consul_policy: + name: bar-access-for-role + rules: | + key "bar" { + policy = "read" + } + key "private/bar" { + policy = "deny" + } + token: "{{ consul_management_token }}" + register: policy_result + +- name: Create a role with policy + consul_role: + name: foo-role-with-policy + policies: + - name: "foo-access-for-role" + token: "{{ consul_management_token }}" + register: result + +- assert: + that: + - result is changed + - result['role']['Name'] == 'foo-role-with-policy' + +- name: Update policy description, in check mode + consul_role: + name: foo-role-with-policy + description: "Testing updating description" + token: "{{ consul_management_token }}" + check_mode: yes + register: result + +- assert: + that: + - result is changed + - result['role']['Description'] == "Testing updating description" + - result['role']['Policies'][0]['Name'] == 'foo-access-for-role' + +- name: Update policy to add the description + consul_role: + name: foo-role-with-policy + description: "Role for testing policies" + token: "{{ consul_management_token }}" + register: result + +- assert: + that: + - result is changed + - result['role']['Description'] == "Role for testing policies" + - result['role']['Policies'][0]['Name'] == 'foo-access-for-role' + +- name: Update the role with another policy, also testing leaving description blank + consul_role: + name: foo-role-with-policy + policies: + - name: "foo-access-for-role" + - name: "bar-access-for-role" + token: "{{ consul_management_token }}" + register: result + +- 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" + +- name: Create a role with service identity + consul_role: + token: "{{ consul_management_token }}" + name: role-with-service-identity + service_identities: + - name: web + datacenters: + - dc1 + register: result + +- assert: + that: + - result is changed + - 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: + token: "{{ consul_management_token }}" + name: role-with-service-identity + service_identities: + - name: web + datacenters: + - dc2 + register: result + check_mode: yes + +- assert: + that: + - result is changed + - 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: + token: "{{ consul_management_token }}" + name: role-with-service-identity + policies: + - name: "foo-access-for-role" + register: result + +- 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' + +- name: Update the role with service identity to remove the policies + consul_role: + token: "{{ consul_management_token }}" + name: role-with-service-identity + policies: [] + register: result + +- assert: + that: + - result is changed + - 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: + token: "{{ consul_management_token }}" + name: role-with-service-identity + node_identities: [] + register: result + check_mode: yes + +- 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 emtpy array + +- name: Update the role with service identity to remove the service identities + consul_role: + token: "{{ consul_management_token }}" + name: role-with-service-identity + service_identities: [] + register: result + +- 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 + +- name: Create a role with node identity + consul_role: + token: "{{ consul_management_token }}" + name: role-with-node-identity + node_identities: + - name: node-1 + datacenter: dc2 + register: result + +- assert: + that: + - result is changed + - 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 +- assert: + that: + - result is changed \ No newline at end of file diff --git a/tests/integration/targets/consul/tasks/main.yml b/tests/integration/targets/consul/tasks/main.yml index 54e40ae4b1..3cd0fef5b8 100644 --- a/tests/integration/targets/consul/tasks/main.yml +++ b/tests/integration/targets/consul/tasks/main.yml @@ -91,6 +91,7 @@ - 3 - import_tasks: consul_session.yml - import_tasks: consul_policy.yml + - import_tasks: consul_role.yml always: - name: Kill consul process shell: kill $(cat {{ remote_tmp_dir }}/consul.pid)