summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/general/plugins/lookup/manifold.py
blob: 049d453e4feab1e5bfa7b843e4ef756a629c3761 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
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))