From f494ff4841714db4ad6d4f7bdd3fcc44b9c261b5 Mon Sep 17 00:00:00 2001 From: The Magician Date: Fri, 16 Aug 2019 10:08:01 -0700 Subject: [PATCH] GcpSession refactor (changing how HTTP requests are sent) (#353) Signed-off-by: Modular Magician --- plugins/module_utils/gcp_utils.py | 103 +++++++--- .../module_utils/gcp/test_gcp_session.py | 189 ++++++++++++++++++ 2 files changed, 261 insertions(+), 31 deletions(-) create mode 100644 test/units/module_utils/gcp/test_gcp_session.py diff --git a/plugins/module_utils/gcp_utils.py b/plugins/module_utils/gcp_utils.py index 1ef97c4..dd4ef01 100644 --- a/plugins/module_utils/gcp_utils.py +++ b/plugins/module_utils/gcp_utils.py @@ -78,53 +78,94 @@ class GcpSession(object): self._validate() def get(self, url, body=None, **kwargs): - kwargs.update({'json': body, 'headers': self._headers()}) - try: - return self.session().get(url, **kwargs) - except getattr(requests.exceptions, 'RequestException') as inst: - self.module.fail_json(msg=inst.message) + """ + This method should be avoided in favor of full_get + """ + kwargs.update({'json': body}) + return self.full_get(url, **kwargs) def post(self, url, body=None, headers=None, **kwargs): - if headers: - headers = self._merge_dictionaries(headers, self._headers()) - else: - headers = self._headers() - - try: - return self.session().post(url, json=body, headers=headers) - except getattr(requests.exceptions, 'RequestException') as inst: - self.module.fail_json(msg=inst.message) + """ + This method should be avoided in favor of full_post + """ + kwargs.update({'json': body, 'headers': headers}) + return self.full_post(url, **kwargs) def post_contents(self, url, file_contents=None, headers=None, **kwargs): - if headers: - headers = self._merge_dictionaries(headers, self._headers()) - else: - headers = self._headers() - - try: - return self.session().post(url, data=file_contents, headers=headers) - except getattr(requests.exceptions, 'RequestException') as inst: - self.module.fail_json(msg=inst.message) + """ + This method should be avoided in favor of full_post + """ + kwargs.update({'data': file_contents, 'headers': headers}) + return self.full_post(url, **kwargs) def delete(self, url, body=None): - try: - return self.session().delete(url, json=body, headers=self._headers()) - except getattr(requests.exceptions, 'RequestException') as inst: - self.module.fail_json(msg=inst.message) + """ + This method should be avoided in favor of full_delete + """ + kwargs = {'json': body} + return self.full_delete(url, **kwargs) def put(self, url, body=None): + """ + This method should be avoided in favor of full_put + """ + kwargs = {'json': body} + return self.full_put(url, **kwargs) + + def patch(self, url, body=None, **kwargs): + """ + This method should be avoided in favor of full_patch + """ + kwargs.update({'json': body}) + return self.full_patch(url, **kwargs) + + # The following methods fully mimic the requests API and should be used. + def full_get(self, url, params=None, **kwargs): + kwargs['headers'] = self._set_headers(kwargs.get('headers')) try: - return self.session().put(url, json=body, headers=self._headers()) + return self.session().get(url, params=params, **kwargs) + except getattr(requests.exceptions, 'RequestException') as inst: + # Only log the message to avoid logging any sensitive info. + self.module.fail_json(msg=inst.message) + + def full_post(self, url, data=None, json=None, **kwargs): + kwargs['headers'] = self._set_headers(kwargs.get('headers')) + + try: + return self.session().post(url, data=data, json=json, **kwargs) except getattr(requests.exceptions, 'RequestException') as inst: self.module.fail_json(msg=inst.message) - def patch(self, url, body=None, **kwargs): - kwargs.update({'json': body, 'headers': self._headers()}) + def full_put(self, url, data=None, **kwargs): + kwargs['headers'] = self._set_headers(kwargs.get('headers')) + try: - return self.session().patch(url, **kwargs) + return self.session().put(url, data=data, **kwargs) except getattr(requests.exceptions, 'RequestException') as inst: self.module.fail_json(msg=inst.message) + def full_patch(self, url, data=None, **kwargs): + kwargs['headers'] = self._set_headers(kwargs.get('headers')) + + try: + return self.session().patch(url, data=data, **kwargs) + except getattr(requests.exceptions, 'RequestException') as inst: + self.module.fail_json(msg=inst.message) + + def full_delete(self, url, **kwargs): + kwargs['headers'] = self._set_headers(kwargs.get('headers')) + + try: + return self.session().delete(url, **kwargs) + except getattr(requests.exceptions, 'RequestException') as inst: + self.module.fail_json(msg=inst.message) + + def _set_headers(self, headers): + if headers: + return self._merge_dictionaries(headers, self._headers()) + else: + return self._headers() + def session(self): return AuthorizedSession( self._credentials()) diff --git a/test/units/module_utils/gcp/test_gcp_session.py b/test/units/module_utils/gcp/test_gcp_session.py new file mode 100644 index 0000000..6d093a5 --- /dev/null +++ b/test/units/module_utils/gcp/test_gcp_session.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +# (c) 2019, Google Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from pytest import importorskip +from units.compat import unittest +from units.compat.mock import patch +from contextlib import contextmanager +from ansible.module_utils.gcp_utils import GcpSession +import responses +import tempfile + +importorskip("requests") +importorskip("google.auth") +importorskip("responses") + +from google.auth.credentials import AnonymousCredentials + + +class FakeModule(object): + def __init__(self, params): + self.params = params + + def fail_json(self, **kwargs): + raise kwargs["msg"] + + +class GcpSessionTestCase(unittest.TestCase): + success_json = {"status": "SUCCESS"} + user_agent = "Google-Ansible-MM-mock" + url = "http://www.googleapis.com/compute/test_instance" + + @contextmanager + def setup_auth(self): + """ + This is a context manager that mocks out + the google-auth library and uses the built-in + AnonymousCredentials for sending requests. + """ + with patch( + "google.oauth2.service_account.Credentials.from_service_account_file" + ) as mock: + with patch.object( + AnonymousCredentials, "with_scopes", create=True + ) as mock2: + creds = AnonymousCredentials() + mock2.return_value = creds + mock.return_value = creds + yield + + @responses.activate + def test_get(self): + responses.add(responses.GET, self.url, status=200, json=self.success_json) + + with self.setup_auth(): + module = FakeModule( + { + "scopes": "foo", + "service_account_file": "file_name", + "project": "test_project", + "auth_kind": "serviceaccount", + } + ) + + session = GcpSession(module, "mock") + resp = session.get(self.url) + + assert responses.calls[0].request.headers["User-Agent"] == self.user_agent + assert resp.json() == self.success_json + assert resp.status_code == 200 + + @responses.activate + def test_post(self): + responses.add(responses.POST, self.url, status=200, json=self.success_json) + + with self.setup_auth(): + body = {"content": "some_content"} + module = FakeModule( + { + "scopes": "foo", + "service_account_file": "file_name", + "project": "test_project", + "auth_kind": "serviceaccount", + } + ) + + session = GcpSession(module, "mock") + resp = session.post( + self.url, body=body, headers={"x-added-header": "my-header"} + ) + + # Ensure Google header added. + assert responses.calls[0].request.headers["User-Agent"] == self.user_agent + + # Ensure all content was passed along. + assert responses.calls[0].request.headers["x-added-header"] == "my-header" + + # Ensure proper request was made. + assert resp.json() == self.success_json + assert resp.status_code == 200 + + @responses.activate + def test_delete(self): + responses.add(responses.DELETE, self.url, status=200, json=self.success_json) + + with self.setup_auth(): + body = {"content": "some_content"} + module = FakeModule( + { + "scopes": "foo", + "service_account_file": "file_name", + "project": "test_project", + "auth_kind": "serviceaccount", + } + ) + + session = GcpSession(module, "mock") + resp = session.delete(self.url) + + # Ensure Google header added. + assert responses.calls[0].request.headers["User-Agent"] == self.user_agent + + # Ensure proper request was made. + assert resp.json() == self.success_json + assert resp.status_code == 200 + + @responses.activate + def test_put(self): + responses.add(responses.PUT, self.url, status=200, json=self.success_json) + + with self.setup_auth(): + body = {"content": "some_content"} + module = FakeModule( + { + "scopes": "foo", + "service_account_file": "file_name", + "project": "test_project", + "auth_kind": "serviceaccount", + } + ) + + session = GcpSession(module, "mock") + resp = session.put(self.url, body={"foo": "bar"}) + + # Ensure Google header added. + assert responses.calls[0].request.headers["User-Agent"] == self.user_agent + + # Ensure proper request was made. + assert resp.json() == self.success_json + assert resp.status_code == 200 + + @responses.activate + def test_patch(self): + responses.add(responses.PATCH, self.url, status=200, json=self.success_json) + + with self.setup_auth(): + body = {"content": "some_content"} + module = FakeModule( + { + "scopes": "foo", + "service_account_file": "file_name", + "project": "test_project", + "auth_kind": "serviceaccount", + } + ) + + session = GcpSession(module, "mock") + resp = session.patch(self.url, body={"foo": "bar"}) + + # Ensure Google header added. + assert responses.calls[0].request.headers["User-Agent"] == self.user_agent + + # Ensure proper request was made. + assert resp.json() == self.success_json + assert resp.status_code == 200