diff options
Diffstat (limited to 'ansible_collections/community/general/plugins/lookup/manifold.py')
-rw-r--r-- | ansible_collections/community/general/plugins/lookup/manifold.py | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/ansible_collections/community/general/plugins/lookup/manifold.py b/ansible_collections/community/general/plugins/lookup/manifold.py new file mode 100644 index 000000000..049d453e4 --- /dev/null +++ b/ansible_collections/community/general/plugins/lookup/manifold.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Arigato Machine Inc. +# Copyright (c) 2018, 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 +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + author: + - Kyrylo Galanov (!UNKNOWN) <galanoff@gmail.com> + name: manifold + short_description: get credentials from Manifold.co + description: + - Retrieves resources' credentials from Manifold.co + options: + _terms: + description: + - Optional list of resource labels to lookup on Manifold.co. If no resources are specified, all + matched resources will be returned. + type: list + elements: string + required: false + api_token: + description: + - manifold API token + type: string + required: true + env: + - name: MANIFOLD_API_TOKEN + project: + description: + - The project label you want to get the resource for. + type: string + required: false + team: + description: + - The team label you want to get the resource for. + type: string + required: false +''' + +EXAMPLES = ''' + - name: all available resources + ansible.builtin.debug: + msg: "{{ lookup('community.general.manifold', api_token='SecretToken') }}" + - name: all available resources for a specific project in specific team + ansible.builtin.debug: + msg: "{{ lookup('community.general.manifold', api_token='SecretToken', project='poject-1', team='team-2') }}" + - name: two specific resources + ansible.builtin.debug: + msg: "{{ lookup('community.general.manifold', 'resource-1', 'resource-2') }}" +''' + +RETURN = ''' + _raw: + description: + - dictionary of credentials ready to be consumed as environment variables. If multiple resources define + the same environment variable(s), the last one returned by the Manifold API will take precedence. + type: dict +''' +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase +from ansible.module_utils.urls import open_url, ConnectionError, SSLValidationError +from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError +from ansible.module_utils.six.moves.urllib.parse import urlencode +from ansible.module_utils import six +from ansible.utils.display import Display +from traceback import format_exception +import json +import sys + +display = Display() + + +class ApiError(Exception): + pass + + +class ManifoldApiClient(object): + base_url = 'https://api.{api}.manifold.co/v1/{endpoint}' + http_agent = 'python-manifold-ansible-1.0.0' + + def __init__(self, token): + self._token = token + + def request(self, api, endpoint, *args, **kwargs): + """ + Send a request to API backend and pre-process a response. + :param api: API to send a request to + :type api: str + :param endpoint: API endpoint to fetch data from + :type endpoint: str + :param args: other args for open_url + :param kwargs: other kwargs for open_url + :return: server response. JSON response is automatically deserialized. + :rtype: dict | list | str + """ + + default_headers = { + 'Authorization': "Bearer {0}".format(self._token), + 'Accept': "*/*" # Otherwise server doesn't set content-type header + } + + url = self.base_url.format(api=api, endpoint=endpoint) + + headers = default_headers + arg_headers = kwargs.pop('headers', None) + if arg_headers: + headers.update(arg_headers) + + try: + display.vvvv('manifold lookup connecting to {0}'.format(url)) + response = open_url(url, headers=headers, http_agent=self.http_agent, *args, **kwargs) + data = response.read() + if response.headers.get('content-type') == 'application/json': + data = json.loads(data) + return data + except ValueError: + raise ApiError('JSON response can\'t be parsed while requesting {url}:\n{json}'.format(json=data, url=url)) + except HTTPError as e: + raise ApiError('Server returned: {err} while requesting {url}:\n{response}'.format( + err=str(e), url=url, response=e.read())) + except URLError as e: + raise ApiError('Failed lookup url for {url} : {err}'.format(url=url, err=str(e))) + except SSLValidationError as e: + raise ApiError('Error validating the server\'s certificate for {url}: {err}'.format(url=url, err=str(e))) + except ConnectionError as e: + raise ApiError('Error connecting to {url}: {err}'.format(url=url, err=str(e))) + + def get_resources(self, team_id=None, project_id=None, label=None): + """ + Get resources list + :param team_id: ID of the Team to filter resources by + :type team_id: str + :param project_id: ID of the project to filter resources by + :type project_id: str + :param label: filter resources by a label, returns a list with one or zero elements + :type label: str + :return: list of resources + :rtype: list + """ + api = 'marketplace' + endpoint = 'resources' + query_params = {} + + if team_id: + query_params['team_id'] = team_id + if project_id: + query_params['project_id'] = project_id + if label: + query_params['label'] = label + + if query_params: + endpoint += '?' + urlencode(query_params) + + return self.request(api, endpoint) + + def get_teams(self, label=None): + """ + Get teams list + :param label: filter teams by a label, returns a list with one or zero elements + :type label: str + :return: list of teams + :rtype: list + """ + api = 'identity' + endpoint = 'teams' + data = self.request(api, endpoint) + # Label filtering is not supported by API, however this function provides uniform interface + if label: + data = list(filter(lambda x: x['body']['label'] == label, data)) + return data + + def get_projects(self, label=None): + """ + Get projects list + :param label: filter projects by a label, returns a list with one or zero elements + :type label: str + :return: list of projects + :rtype: list + """ + api = 'marketplace' + endpoint = 'projects' + query_params = {} + + if label: + query_params['label'] = label + + if query_params: + endpoint += '?' + urlencode(query_params) + + return self.request(api, endpoint) + + def get_credentials(self, resource_id): + """ + Get resource credentials + :param resource_id: ID of the resource to filter credentials by + :type resource_id: str + :return: + """ + api = 'marketplace' + endpoint = 'credentials?' + urlencode({'resource_id': resource_id}) + return self.request(api, endpoint) + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + """ + :param terms: a list of resources lookups to run. + :param variables: ansible variables active at the time of the lookup + :param api_token: API token + :param project: optional project label + :param team: optional team label + :return: a dictionary of resources credentials + """ + + self.set_options(var_options=variables, direct=kwargs) + + api_token = self.get_option('api_token') + project = self.get_option('project') + team = self.get_option('team') + + try: + labels = terms + client = ManifoldApiClient(api_token) + + if team: + team_data = client.get_teams(team) + if len(team_data) == 0: + raise AnsibleError("Team '{0}' does not exist".format(team)) + team_id = team_data[0]['id'] + else: + team_id = None + + if project: + project_data = client.get_projects(project) + if len(project_data) == 0: + raise AnsibleError("Project '{0}' does not exist".format(project)) + project_id = project_data[0]['id'] + else: + project_id = None + + if len(labels) == 1: # Use server-side filtering if one resource is requested + resources_data = client.get_resources(team_id=team_id, project_id=project_id, label=labels[0]) + else: # Get all resources and optionally filter labels + resources_data = client.get_resources(team_id=team_id, project_id=project_id) + if labels: + resources_data = list(filter(lambda x: x['body']['label'] in labels, resources_data)) + + if labels and len(resources_data) < len(labels): + fetched_labels = [r['body']['label'] for r in resources_data] + not_found_labels = [label for label in labels if label not in fetched_labels] + raise AnsibleError("Resource(s) {0} do not exist".format(', '.join(not_found_labels))) + + credentials = {} + cred_map = {} + for resource in resources_data: + resource_credentials = client.get_credentials(resource['id']) + if len(resource_credentials) and resource_credentials[0]['body']['values']: + for cred_key, cred_val in six.iteritems(resource_credentials[0]['body']['values']): + label = resource['body']['label'] + if cred_key in credentials: + display.warning("'{cred_key}' with label '{old_label}' was replaced by resource data " + "with label '{new_label}'".format(cred_key=cred_key, + old_label=cred_map[cred_key], + new_label=label)) + credentials[cred_key] = cred_val + cred_map[cred_key] = label + + ret = [credentials] + return ret + except ApiError as e: + raise AnsibleError('API Error: {0}'.format(str(e))) + except AnsibleError as e: + raise e + except Exception: + exc_type, exc_value, exc_traceback = sys.exc_info() + raise AnsibleError(format_exception(exc_type, exc_value, exc_traceback)) |