From 15a4fbbba49312b5f049d515db0a54240bdc7f58 Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Fri, 16 Aug 2024 06:40:21 +0200 Subject: [PATCH] Add credential and credential_info modules (#251) Signed-off-by: Alina Buzachis --- .config/dictionary.txt | 2 + meta/runtime.yml | 2 + plugins/module_utils/controller.py | 6 + plugins/modules/credential.py | 200 ++++++++++++++++++ plugins/modules/credential_info.py | 118 +++++++++++ .../targets/credential/tasks/main.yml | 197 +++++++++++++++++ 6 files changed, 525 insertions(+) create mode 100644 plugins/modules/credential.py create mode 100644 plugins/modules/credential_info.py create mode 100644 tests/integration/targets/credential/tasks/main.yml diff --git a/.config/dictionary.txt b/.config/dictionary.txt index d14e4a3e..8dc43434 100644 --- a/.config/dictionary.txt +++ b/.config/dictionary.txt @@ -60,3 +60,5 @@ Alina Buzachis alinabuzachis hdrs +testuser +testsecret diff --git a/meta/runtime.yml b/meta/runtime.yml index 92e747a8..13fef1cd 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -4,5 +4,7 @@ requires_ansible: ">=2.15.0" # AAP 2.4 or newer action_groups: eda: + - credential + - credential_info - credential_type - credential_type_info diff --git a/plugins/module_utils/controller.py b/plugins/module_utils/controller.py index 54c49c30..43e3f853 100644 --- a/plugins/module_utils/controller.py +++ b/plugins/module_utils/controller.py @@ -81,6 +81,12 @@ def fail_wanted_one(self, response, endpoint, query_params): msg = f"Request to {display_endpoint} returned {response.json['count']} items, expected 1" raise EDAError(msg) + def get_exactly_one(self, endpoint, name=None, **kwargs): + return self.get_one_or_many(endpoint, name=name, allow_none=False, **kwargs) + + def resolve_name_to_id(self, endpoint, name): + return self.get_exactly_one(endpoint, name)["id"] + def get_one_or_many( self, endpoint, diff --git a/plugins/modules/credential.py b/plugins/modules/credential.py new file mode 100644 index 00000000..4233a611 --- /dev/null +++ b/plugins/modules/credential.py @@ -0,0 +1,200 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: credential +author: + - "Nikhil Jain (@jainnikhil30)" + - "Alina Buzachis (@alinabuzachis)" +short_description: Manage credentials in EDA Controller +description: + - This module allows the user to create, update or delete a credential in EDA controller. +version_added: 2.0.0 +options: + name: + description: + - Name of the credential. + type: str + required: true + new_name: + description: + - Setting this option will change the existing name (lookup via name). + type: str + inputs: + description: + - Credential inputs where the keys are var names used in templating. + type: dict + credential_type_name: + description: + - The name of the credential type. + type: str + organization_name: + description: + - The name of the organization. + type: int + aliases: + - org_name + description: + description: + - Description of the credential. + type: str + state: + description: + - Desired state of the resource. + default: "present" + choices: ["present", "absent"] + type: str +extends_documentation_fragment: + - ansible.eda.eda_controller.auths +""" + + +EXAMPLES = """ +- name: Create an EDA Credential + ansible.eda.credential: + name: "Example Credential" + description: "Example credential description" + inputs: + field1: "field1" + credential_type_name: "GitLab Personal Access Token" + +- name: Delete an EDA Credential + ansible.eda.credential: + name: "Example Credential" + state: absent +""" + + +RETURN = """ +id: + description: ID of the credential. + returned: when exists + type: int + sample: 24 +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.arguments import AUTH_ARGSPEC +from ..module_utils.client import Client +from ..module_utils.controller import Controller +from ..module_utils.errors import EDAError + + +def lookup(module, controller, endpoint, name): + result = None + try: + result = controller.resolve_name_to_id(endpoint, name) + except EDAError as e: + module.fail_json(msg=f"Failed to lookup resource: {e}") + return result + + +def main(): + argument_spec = dict( + name=dict(type="str", required=True), + new_name=dict(type="str"), + description=dict(type="str"), + inputs=dict(type="dict"), + credential_type_name=dict(type="str"), + organization_name=dict(type="int", aliases=["org_name"]), + state=dict(choices=["present", "absent"], default="present"), + ) + + argument_spec.update(AUTH_ARGSPEC) + + required_if = [("state", "present", ("name", "credential_type_name", "inputs"))] + + module = AnsibleModule( + argument_spec=argument_spec, required_if=required_if, supports_check_mode=True + ) + + client = Client( + host=module.params.get("controller_host"), + username=module.params.get("controller_username"), + password=module.params.get("controller_password"), + timeout=module.params.get("request_timeout"), + validate_certs=module.params.get("validate_certs"), + ) + + controller = Controller(client, module) + + name = module.params.get("name") + new_name = module.params.get("new_name") + state = module.params.get("state") + + credential_params = {} + if module.params.get("description"): + credential_params["description"] = module.params["description"] + + if module.params.get("inputs"): + credential_params["inputs"] = module.params["inputs"] + + credential_type_id = None + if module.params.get("credential_type_name"): + credential_type_id = lookup( + module, + controller, + "credential-types", + module.params["credential_type_name"], + ) + + if credential_type_id: + credential_params["credential_type_id"] = credential_type_id + + organization_id = None + if module.params.get("organization_id"): + organization_id = lookup( + module, controller, "organizations", module.params["organization_name"] + ) + + if organization_id: + credential_params["organization_id"] = organization_id + + # Attempt to look up credential based on the provided name + try: + credential = controller.get_one_or_many("eda-credentials", name=name) + except EDAError as e: + module.fail_json(msg=f"Failed to get credential: {e}") + + if state == "absent": + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + try: + result = controller.delete_if_needed(credential, endpoint="eda-credentials") + module.exit_json(**result) + except EDAError as e: + module.fail_json(msg=f"Failed to delete credential: {e}") + + credential_params["name"] = ( + new_name + if new_name + else (controller.get_item_name(credential) if credential else name) + ) + + # If the state was present and we can let the module build or update the + # existing credential, this will return on its own + try: + result = controller.create_or_update_if_needed( + credential, + credential_params, + endpoint="eda-credentials", + item_type="credential", + ) + module.exit_json(**result) + except EDAError as e: + module.fail_json(msg=f"Failed to create/update credential: {e}") + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/credential_info.py b/plugins/modules/credential_info.py new file mode 100644 index 00000000..3d4ffb3d --- /dev/null +++ b/plugins/modules/credential_info.py @@ -0,0 +1,118 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = """ +--- +module: credential_info +author: + - Alina Buzachis (@alinabuzachis) +short_description: List credentials in the EDA Controller +description: + - List credentials in the EDA controller. +version_added: 2.0.0 +options: + name: + description: + - The name of the credential. + type: str + required: false +extends_documentation_fragment: + - ansible.eda.eda_controller.auths +""" + + +EXAMPLES = """ + - name: Get information about a credential + ansible.eda.credential_info: + name: "Test" + + - name: List all credentials + ansible.eda.credential_info: +""" + + +RETURN = """ +credentials: + description: Information about credentials. + returned: always + type: list + elements: dict + sample: [ + { + "created_at": "2024-08-14T08:57:55.151787Z", + "credential_type": { + "id": 1, + "kind": "scm", + "name": "Source Control", + "namespace": "scm" + }, + "description": "This is a test credential", + "id": 24, + "inputs": { + "password": "$encrypted$", + "username": "testuser" + }, + "managed": false, + "modified_at": "2024-08-14T08:57:56.324925Z", + "name": "New Test Credential", + "organization": { + "description": "The default organization", + "id": 1, + "name": "Default" + }, + "references": null + } + ] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.arguments import AUTH_ARGSPEC +from ..module_utils.client import Client +from ..module_utils.common import to_list_of_dict +from ..module_utils.controller import Controller +from ..module_utils.errors import EDAError + + +def main(): + argument_spec = dict( + name=dict(type="str", required=False), + ) + + argument_spec.update(AUTH_ARGSPEC) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + client = Client( + host=module.params.get("controller_host"), + username=module.params.get("controller_username"), + password=module.params.get("controller_password"), + timeout=module.params.get("request_timeout"), + validate_certs=module.params.get("validate_certs"), + ) + + name = module.params.get("name") + controller = Controller(client, module) + + # Attempt to look up credential based on the provided name + try: + result = controller.get_one_or_many( + "eda-credentials", name=name, want_one=False + ) + except EDAError as e: + module.fail_json(msg=f"Failed to get credential: {e}") + + module.exit_json(credentials=to_list_of_dict(result)) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/credential/tasks/main.yml b/tests/integration/targets/credential/tasks/main.yml new file mode 100644 index 00000000..7952cfa1 --- /dev/null +++ b/tests/integration/targets/credential/tasks/main.yml @@ -0,0 +1,197 @@ +--- +- block: + - set_fact: + credential_defaults: &credential_defaults + controller_username: "{{ controller_username }}" + controller_password: "{{ controller_password }}" + controller_host: "{{ controller_host }}" + validate_certs: false + + - name: Generate a random_string for the test + set_fact: + random_string: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: random_string is not defined + + - name: Generate a ID for the test + set_fact: + test_id: "{{ random_string | to_uuid }}" + when: test_id is not defined + + - name: Define variables for credential and project + set_fact: + credential_name: "Test_Credential_{{ test_id }}" + new_credential_name: "New_Test_Credential_{{ test_id }}" + credential_type_name: "Test_CredentialType_{{ test_id }}" + + # CREATE + - name: Create credential type + ansible.eda.credential_type: + <<: *credential_defaults + name: "{{ credential_type_name }}" + state: present + description: "A test credential type" + inputs: + fields: + - id: "field1" + type: "string" + label: "Field 5" + injectors: + extra_vars: + field1: "field1" + + - name: Create credential in check mode + ansible.eda.credential: + <<: *credential_defaults + state: present + name: "{{ credential_name }}" + description: "This is a test credential" + credential_type_name: "{{ credential_type_name }}" + inputs: + field1: "field1" + check_mode: true + register: _result + + - name: Check credential creation in check mode + assert: + that: + - _result.changed + + - name: Create credential + ansible.eda.credential: + <<: *credential_defaults + state: present + name: "{{ credential_name }}" + description: "This is a test credential" + credential_type_name: "{{ credential_type_name }}" + inputs: + field1: "field1" + register: _result + + - name: Check credential creation + assert: + that: + - _result.changed + + - name: Create credential again + ansible.eda.credential: + <<: *credential_defaults + state: present + name: "{{ credential_name }}" + description: "This is a test credential" + credential_type_name: "{{ credential_type_name }}" + inputs: + field1: "field1" + register: _result + + # [WARNING]: The field inputs of unknown 3 has encrypted data and may inaccurately report task is changed. + - name: Check credential is not created again + assert: + that: + - not _result.changed + ignore_errors: true + + - name: Get info about a credential + ansible.eda.credential_info: + <<: *credential_defaults + name: "{{ credential_name }}" + + # UPDATE + - name: Update credential name + ansible.eda.credential: + <<: *credential_defaults + state: present + name: "{{ new_credential_name }}" + description: "This is a test credential" + credential_type_name: "{{ credential_type_name }}" + inputs: + field1: "field1" + register: _result + + - name: Check credential update + assert: + that: + - _result.changed + + - name: Update credential again + ansible.eda.credential: + <<: *credential_defaults + state: present + name: "{{ new_credential_name }}" + description: "This is a test credential" + credential_type_name: "{{ credential_type_name }}" + inputs: + field1: "field1" + register: _result + + # [WARNING]: The field inputs of unknown 3 has encrypted data and may inaccurately report task is changed. + - name: Check credential is not updated again + assert: + that: + - not _result.changed + ignore_errors: true + + - name: Get info about credential + ansible.eda.credential_info: + <<: *credential_defaults + name: "{{ new_credential_name }}" + + - name: List all credentials + ansible.eda.credential_info: + <<: *credential_defaults + + # DELETE + - name: Delete operation type without required name parameter + ansible.eda.credential: + <<: *credential_defaults + state: absent + ignore_errors: true + register: _result + + - name: Check if credential name is required + assert: + that: + - _result.failed + - "'missing required arguments: name' in _result.msg" + + - name: Delete non-existing credential in check mode + ansible.eda.credential: + <<: *credential_defaults + name: "Example2" + state: absent + check_mode: true + register: _result + + - name: Check if delete non-existing credential in check mode + assert: + that: + - not _result.changed + + - name: Delete credential + ansible.eda.credential: + <<: *credential_defaults + name: "{{ new_credential_name }}" + state: absent + register: _result + + - name: Check if delete non-existing credential + assert: + that: + - _result.changed + + always: + - name: Clean up - credential + ansible.eda.credential: + <<: *credential_defaults + name: "{{ item }}" + state: absent + loop: + - "{{ credential_name }}" + - "{{ new_credential_name }}" + ignore_errors: true + + - name: Clean up - credential type + ansible.eda.credential_type: + <<: *credential_defaults + name: "{{ credential_type_name }}" + state: absent + ignore_errors: true