diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 388900a06b..b98d91b905 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -116,6 +116,9 @@ Fixed CentOS. (bug fix) #4297 * Fix a bug with action runner throwing an exception and failing to run an action if there was an empty pack config inside ``/opt/stackstorm/configs/``. (bug fix) #4325 +* Update ``GET /v1/actions/views/entry_point/`` to return correct ``Content-Type`` + response header based on the entry point type / file extension. Previously it would always + incorrectly return ``application/json``. (improvement) #4327 Deprecated ~~~~~~~~~~ diff --git a/st2api/st2api/controllers/v1/action_views.py b/st2api/st2api/controllers/v1/action_views.py index 762586cb55..36b2025c11 100644 --- a/st2api/st2api/controllers/v1/action_views.py +++ b/st2api/st2api/controllers/v1/action_views.py @@ -13,8 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -from mongoengine import ValidationError +import os +import codecs +import mimetypes + import six +from mongoengine import ValidationError from st2api.controllers import resource from st2common.exceptions.db import StackStormDBObjectNotFoundError @@ -27,6 +31,7 @@ from st2common.rbac.types import PermissionType from st2common.rbac import utils as rbac_utils from st2common.router import abort +from st2common.router import Response __all__ = [ 'OverviewController', @@ -186,10 +191,32 @@ def get_one(self, ref_or_id, requester_user): raise StackStormDBObjectNotFoundError('Action ref_or_id=%s has no entry_point to output' % ref_or_id) - with open(abs_path) as file: - content = file.read() + with codecs.open(abs_path, 'r') as fp: + content = fp.read() - return content + # Ensure content is utf-8 + if isinstance(content, six.binary_type): + content = content.decode('utf-8') + + try: + content_type = mimetypes.guess_type(abs_path)[0] + except Exception: + content_type = None + + # Special case if /etc/mime.types doesn't contain entry for yaml, py + if not content_type: + _, extension = os.path.splitext(abs_path) + if extension in ['.yaml', '.yml']: + content_type = 'application/x-yaml' + elif extension in ['.py']: + content_type = 'application/x-python' + else: + content_type = 'text/plain' + + response = Response() + response.headers['Content-Type'] = content_type + response.text = content + return response class ActionViewsController(object): diff --git a/st2api/tests/unit/controllers/v1/test_action_views.py b/st2api/tests/unit/controllers/v1/test_action_views.py index b1897f0a21..2569f7b520 100644 --- a/st2api/tests/unit/controllers/v1/test_action_views.py +++ b/st2api/tests/unit/controllers/v1/test_action_views.py @@ -212,3 +212,51 @@ def test_get_one_ref(self): self.assertEqual(get_resp.status_int, 200) finally: self.app.delete('/v1/actions/%s' % action_id) + + @mock.patch.object(action_validator, 'validate_action', mock.MagicMock( + return_value=True)) + @mock.patch.object(content_utils, 'get_entry_point_abs_path', mock.MagicMock( + return_value='/path/to/file.yaml')) + @mock.patch(mock_open_name, mock.mock_open(read_data='file content'), create=True) + def test_get_one_ref_yaml_content_type(self): + post_resp = self.app.post_json('/v1/actions', ACTION_1) + action_id = post_resp.json['id'] + action_ref = '.'.join((post_resp.json['pack'], post_resp.json['name'])) + try: + get_resp = self.app.get('/v1/actions/views/entry_point/%s' % action_ref) + self.assertEqual(get_resp.status_int, 200) + self.assertEqual(get_resp.headers['Content-Type'], 'application/x-yaml') + finally: + self.app.delete('/v1/actions/%s' % action_id) + + @mock.patch.object(action_validator, 'validate_action', mock.MagicMock( + return_value=True)) + @mock.patch.object(content_utils, 'get_entry_point_abs_path', mock.MagicMock( + return_value=__file__.replace('.pyc', '.py'))) + @mock.patch(mock_open_name, mock.mock_open(read_data='file content'), create=True) + def test_get_one_ref_python_content_type(self): + post_resp = self.app.post_json('/v1/actions', ACTION_1) + action_id = post_resp.json['id'] + action_ref = '.'.join((post_resp.json['pack'], post_resp.json['name'])) + try: + get_resp = self.app.get('/v1/actions/views/entry_point/%s' % action_ref) + self.assertEqual(get_resp.status_int, 200) + self.assertEqual(get_resp.headers['Content-Type'], 'application/x-python') + finally: + self.app.delete('/v1/actions/%s' % action_id) + + @mock.patch.object(action_validator, 'validate_action', mock.MagicMock( + return_value=True)) + @mock.patch.object(content_utils, 'get_entry_point_abs_path', mock.MagicMock( + return_value='/file/does/not/exist')) + @mock.patch(mock_open_name, mock.mock_open(read_data='file content'), create=True) + def test_get_one_ref_text_plain_content_type(self): + post_resp = self.app.post_json('/v1/actions', ACTION_1) + action_id = post_resp.json['id'] + action_ref = '.'.join((post_resp.json['pack'], post_resp.json['name'])) + try: + get_resp = self.app.get('/v1/actions/views/entry_point/%s' % action_ref) + self.assertEqual(get_resp.status_int, 200) + self.assertEqual(get_resp.headers['Content-Type'], 'text/plain') + finally: + self.app.delete('/v1/actions/%s' % action_id) diff --git a/st2common/st2common/router.py b/st2common/st2common/router.py index a969964be4..ad33ecb68b 100644 --- a/st2common/st2common/router.py +++ b/st2common/st2common/router.py @@ -570,7 +570,12 @@ def __init__(self, **entries): try: validator = CustomValidator(response_spec['schema'], resolver=self.spec_resolver) - validator.validate(resp.json) + + response_type = response_spec['schema'].get('type', 'json') + if response_type == 'string': + validator.validate(resp.text) + else: + validator.validate(resp.json) except (jsonschema.ValidationError, ValueError): LOG.exception('Response validation failed.') resp.headers.add('Warning', '199 OpenAPI "Response validation failed"')