Add validation logic to Test endpoints

This PS adds validation logic recently implemented in
armada.utils.validate [0] for validating documents and
Armada-generated Manifests to the Test and Tests controller classes.

Also refactors some exception handling for both controller classes to
better bubble up the appropriate exception.

Finally unit tests have been added for the Armada Test controller
to verify above changes work.

[0] https://review.gerrithub.io/#/c/378700/

Change-Id: I01f73c1778bf7c2e38032d5fddabd327c013edbb
This commit is contained in:
Felipe Monteiro 2018-02-23 20:44:00 +00:00 committed by Stacey Fletcher
parent 18316ff6c9
commit e4a4d2ea68
8 changed files with 278 additions and 101 deletions

View File

@ -17,6 +17,8 @@ import logging as log
import uuid import uuid
import yaml import yaml
from oslo_utils import excutils
import falcon import falcon
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
@ -41,23 +43,22 @@ class BaseResource(object):
resp.headers['Allow'] = ','.join(allowed_methods) resp.headers['Allow'] = ','.join(allowed_methods)
resp.status = falcon.HTTP_200 resp.status = falcon.HTTP_200
def req_yaml(self, req): def req_yaml(self, req, default=None):
if req.content_length is None or req.content_length == 0: if req.content_length is None or req.content_length == 0:
return None return default
raw_body = req.stream.read(req.content_length or 0) raw_body = req.stream.read(req.content_length or 0)
if raw_body is None: if raw_body is None:
return None return default
try: try:
return yaml.safe_load_all(raw_body.decode('utf-8')) return list(yaml.safe_load_all(raw_body.decode('utf-8')))
except yaml.YAMLError as jex: except yaml.YAMLError as jex:
with excutils.save_and_reraise_exception():
self.error( self.error(
req.context, req.context,
"Invalid YAML in request: \n%s" % raw_body.decode('utf-8')) "Invalid YAML in request: \n%s" % raw_body.decode('utf-8'))
raise Exception(
"%s: Invalid YAML in body: %s" % (req.path, jex))
def req_json(self, req): def req_json(self, req):
if req.content_length is None or req.content_length == 0: if req.content_length is None or req.content_length == 0:

View File

@ -13,6 +13,7 @@
# limitations under the License. # limitations under the License.
import json import json
import yaml
import falcon import falcon
from oslo_config import cfg from oslo_config import cfg
@ -23,19 +24,20 @@ from armada import const
from armada.handlers.tiller import Tiller from armada.handlers.tiller import Tiller
from armada.handlers.manifest import Manifest from armada.handlers.manifest import Manifest
from armada.utils.release import release_prefix from armada.utils.release import release_prefix
from armada.utils import validate
CONF = cfg.CONF CONF = cfg.CONF
class Test(api.BaseResource): class Test(api.BaseResource):
''' '''
Test helm releases via release name Test Helm releases via release name.
''' '''
@policy.enforce('armada:test_release') @policy.enforce('armada:test_release')
def on_get(self, req, resp, release): def on_get(self, req, resp, release):
try:
self.logger.info('RUNNING: %s', release) self.logger.info('RUNNING: %s', release)
try:
tiller = Tiller( tiller = Tiller(
tiller_host=req.get_param('tiller_host'), tiller_host=req.get_param('tiller_host'),
tiller_port=req.get_param_as_int( tiller_port=req.get_param_as_int(
@ -43,6 +45,13 @@ class Test(api.BaseResource):
tiller_namespace=req.get_param( tiller_namespace=req.get_param(
'tiller_namespace', default=CONF.tiller_namespace)) 'tiller_namespace', default=CONF.tiller_namespace))
tiller_resp = tiller.testing_release(release) tiller_resp = tiller.testing_release(release)
# TODO(fmontei): Provide more sensible exception(s) here.
except Exception as e:
err_message = 'Failed to test {}: {}'.format(release, e)
self.error(req.context, err_message)
return self.return_error(
resp, falcon.HTTP_500, message=err_message)
msg = { msg = {
'result': '', 'result': '',
'message': '' 'message': ''
@ -68,20 +77,55 @@ class Test(api.BaseResource):
resp.status = falcon.HTTP_200 resp.status = falcon.HTTP_200
resp.content_type = 'application/json' resp.content_type = 'application/json'
except Exception as e:
err_message = 'Failed to test {}: {}'.format(release, e)
self.error(req.context, err_message)
self.return_error(
resp, falcon.HTTP_500, message=err_message)
class Tests(api.BaseResource): class Tests(api.BaseResource):
''' '''
Test helm releases via a manifest Test Helm releases via a Manifest.
''' '''
def _format_validation_response(self, req, resp, result, details):
resp.content_type = 'application/json'
resp_body = {
'kind': 'Status',
'apiVersion': 'v1.0',
'metadata': {},
'reason': 'Validation',
'details': {},
}
error_details = [m for m in details if m.get('error', False)]
resp_body['details']['errorCount'] = len(error_details)
resp_body['details']['messageList'] = details
if result:
resp.status = falcon.HTTP_200
resp_body['status'] = 'Success'
resp_body['message'] = 'Armada validations succeeded.'
resp_body['code'] = 200
else:
resp.status = falcon.HTTP_400
resp_body['status'] = 'Failure'
resp_body['message'] = (
'Failed to validate documents or generate Armada Manifest '
'from documents.')
resp_body['code'] = 400
self.error(req.context, resp_body['message'])
resp.body = json.dumps(resp_body)
return result
def _validate_documents(self, req, resp, documents):
result, details = validate.validate_armada_documents(documents)
return self._format_validation_response(req, resp, result,
details)
@policy.enforce('armada:tests_manifest') @policy.enforce('armada:tests_manifest')
def on_post(self, req, resp): def on_post(self, req, resp):
# TODO(fmontei): Validation Content-Type is application/x-yaml.
target_manifest = req.get_param('target_manifest', None)
try: try:
tiller = Tiller( tiller = Tiller(
tiller_host=req.get_param('tiller_host'), tiller_host=req.get_param('tiller_host'),
@ -89,11 +133,27 @@ class Tests(api.BaseResource):
'tiller_port') or CONF.tiller_port, 'tiller_port') or CONF.tiller_port,
tiller_namespace=req.get_param( tiller_namespace=req.get_param(
'tiller_namespace', default=CONF.tiller_namespace)) 'tiller_namespace', default=CONF.tiller_namespace))
# TODO(fmontei): Provide more sensible exception(s) here.
except Exception as e:
err_message = 'Failed to initialize Tiller handler.'
self.error(req.context, err_message)
return self.return_error(
resp, falcon.HTTP_500, message=err_message)
try:
documents = self.req_yaml(req, default=[])
except yaml.YAMLError:
err_message = 'Documents must be valid YAML.'
return self.return_error(
resp, falcon.HTTP_400, message=err_message)
is_valid = self._validate_documents(req, resp, documents)
if not is_valid:
return resp
documents = self.req_yaml(req)
target_manifest = req.get_param('target_manifest', None)
armada_obj = Manifest( armada_obj = Manifest(
documents, target_manifest=target_manifest).get_manifest() documents, target_manifest=target_manifest).get_manifest()
prefix = armada_obj.get(const.KEYWORD_ARMADA).get( prefix = armada_obj.get(const.KEYWORD_ARMADA).get(
const.KEYWORD_PREFIX) const.KEYWORD_PREFIX)
known_releases = [release[0] for release in tiller.list_charts()] known_releases = [release[0] for release in tiller.list_charts()]
@ -134,12 +194,5 @@ class Tests(api.BaseResource):
message['test']['skipped'].append(release_name) message['test']['skipped'].append(release_name)
resp.status = falcon.HTTP_200 resp.status = falcon.HTTP_200
resp.body = json.dumps(message) resp.body = json.dumps(message)
resp.content_type = 'application/json' resp.content_type = 'application/json'
except Exception as e:
err_message = 'Failed to test manifest: {}'.format(e)
self.error(req.context, err_message)
self.return_error(
resp, falcon.HTTP_500, message=err_message)

View File

@ -29,9 +29,9 @@ class ArmadaBaseException(Exception):
def __init__(self, message=None, **kwargs): def __init__(self, message=None, **kwargs):
self.message = message or self.message self.message = message or self.message
try: try: # nosec
self.message = self.message % kwargs self.message = self.message % kwargs
except TypeError: except Exception:
pass pass
super(ArmadaBaseException, self).__init__(self.message) super(ArmadaBaseException, self).__init__(self.message)

View File

@ -12,11 +12,124 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json
import os
import yaml
import mock
from armada.api.controller import test
from armada.common.policies import base as policy_base from armada.common.policies import base as policy_base
from armada.exceptions import manifest_exceptions
from armada.tests import test_utils from armada.tests import test_utils
from armada.tests.unit.api import base from armada.tests.unit.api import base
class TestControllerTest(base.BaseControllerTest):
@mock.patch.object(test, 'Manifest')
@mock.patch.object(test, 'Tiller')
def test_test_controller_with_manifest(self, mock_tiller, mock_manifest):
rules = {'armada:tests_manifest': '@'}
self.policy.set_rules(rules)
manifest_path = os.path.join(os.getcwd(), 'examples',
'keystone-manifest.yaml')
with open(manifest_path, 'r') as f:
payload = f.read()
documents = list(yaml.safe_load_all(payload))
resp = self.app.simulate_post('/api/v1.0/tests', body=payload)
self.assertEqual(200, resp.status_code)
result = json.loads(resp.text)
expected = {
"tests": {"passed": [], "skipped": [], "failed": []}
}
self.assertEqual(expected, result)
mock_manifest.assert_called_once_with(
documents, target_manifest=None)
self.assertTrue(mock_tiller.called)
@test_utils.attr(type=['negative'])
class TestControllerNegativeTest(base.BaseControllerTest):
@mock.patch.object(test, 'Manifest')
@mock.patch.object(test, 'Tiller')
def test_test_controller_tiller_exc_returns_500(self, mock_tiller, _):
rules = {'armada:tests_manifest': '@'}
self.policy.set_rules(rules)
mock_tiller.side_effect = Exception
resp = self.app.simulate_post('/api/v1.0/tests')
self.assertEqual(500, resp.status_code)
@mock.patch.object(test, 'Manifest')
@mock.patch.object(test, 'Tiller')
def test_test_controller_validation_failure_returns_400(
self, *_):
rules = {'armada:tests_manifest': '@'}
self.policy.set_rules(rules)
manifest_path = os.path.join(os.getcwd(), 'examples',
'keystone-manifest.yaml')
with open(manifest_path, 'r') as f:
payload = f.read()
documents = list(yaml.safe_load_all(payload))
documents[0]['schema'] = 'totally-invalid'
invalid_payload = yaml.safe_dump_all(documents)
resp = self.app.simulate_post('/api/v1.0/tests', body=invalid_payload)
self.assertEqual(400, resp.status_code)
resp_body = json.loads(resp.text)
self.assertEqual(400, resp_body['code'])
self.assertEqual(1, resp_body['details']['errorCount'])
self.assertIn(
{'message': (
'An error occurred while generating the manifest: Could not '
'find dependency chart helm-toolkit in armada/Chart/v1.'),
'error': True},
resp_body['details']['messageList'])
self.assertEqual(('Failed to validate documents or generate Armada '
'Manifest from documents.'),
resp_body['message'])
@mock.patch('armada.utils.validate.Manifest')
@mock.patch.object(test, 'Tiller')
def test_test_controller_manifest_failure_returns_400(
self, _, mock_manifest):
rules = {'armada:tests_manifest': '@'}
self.policy.set_rules(rules)
mock_manifest.return_value.get_manifest.side_effect = (
manifest_exceptions.ManifestException(details='foo'))
manifest_path = os.path.join(os.getcwd(), 'examples',
'keystone-manifest.yaml')
with open(manifest_path, 'r') as f:
payload = f.read()
resp = self.app.simulate_post('/api/v1.0/tests', body=payload)
self.assertEqual(400, resp.status_code)
resp_body = json.loads(resp.text)
self.assertEqual(400, resp_body['code'])
self.assertEqual(1, resp_body['details']['errorCount'])
self.assertEqual(
[{'message': (
'An error occurred while generating the manifest: foo.'),
'error': True}],
resp_body['details']['messageList'])
self.assertEqual(('Failed to validate documents or generate Armada '
'Manifest from documents.'),
resp_body['message'])
class TestControllerNegativeRbacTest(base.BaseControllerTest): class TestControllerNegativeRbacTest(base.BaseControllerTest):
@test_utils.attr(type=['negative']) @test_utils.attr(type=['negative'])

View File

@ -22,8 +22,8 @@ Armada Exceptions
.. include:: base-exceptions.inc .. include:: base-exceptions.inc
.. include:: chartbuilder-exceptions.inc .. include:: chartbuilder-exceptions.inc
.. include:: k8s-exceptions.inc .. include:: k8s-exceptions.inc
.. include:: lint-exceptions.inc
.. include:: manifest-exceptions.inc .. include:: manifest-exceptions.inc
.. include:: override-exceptions.inc .. include:: override-exceptions.inc
.. include:: source-exceptions.inc .. include:: source-exceptions.inc
.. include:: tiller-exceptions.inc .. include:: tiller-exceptions.inc
.. include:: validate-exceptions.inc

View File

@ -26,8 +26,8 @@
:members: :members:
:show-inheritance: :show-inheritance:
:undoc-members: :undoc-members:
* - KubernetesUnknownStreamngEventTypeException * - KubernetesUnknownStreamingEventTypeException
- .. autoexception:: armada.exceptions.k8s_exceptions.KubernetesUnknownStreamngEventTypeException - .. autoexception:: armada.exceptions.k8s_exceptions.KubernetesUnknownStreamingEventTypeException
:members: :members:
:show-inheritance: :show-inheritance:
:undoc-members: :undoc-members:

View File

@ -20,13 +20,23 @@
* - Exception Name * - Exception Name
- Description - Description
* - InvalidArmadaObjectException
- .. autoexception:: armada.exceptions.lint_exceptions.InvalidArmadaObjectException
:members:
:show-inheritance:
:undoc-members:
* - InvalidManifestException * - InvalidManifestException
- .. autoexception:: armada.exceptions.lint_exceptions.InvalidManifestException - .. autoexception:: armada.exceptions.validate_exceptions.InvalidManifestException
:members:
:show-inheritance:
:undoc-members:
* - InvalidChartDefinitionException
- .. autoexception:: armada.exceptions.validate_exceptions.InvalidChartDefinitionException
:members:
:show-inheritance:
:undoc-members:
* - InvalidReleaseException
- .. autoexception:: armada.exceptions.validate_exceptions.InvalidReleaseException
:members:
:show-inheritance:
:undoc-members:
* - InvalidArmadaObjectException
- .. autoexception:: armada.exceptions.validate_exceptions.InvalidArmadaObjectException
:members: :members:
:show-inheritance: :show-inheritance:
:undoc-members: :undoc-members:

View File

@ -19,7 +19,7 @@ set -ex
CMD="armada" CMD="armada"
# Define port # Define port
PORT=${ARMADA_API_PORT:-9000} PORT=${ARMADA_API_PORT:-8000}
# How long uWSGI should wait for each Armada response # How long uWSGI should wait for each Armada response
ARMADA_API_TIMEOUT=${ARMADA_API_TIMEOUT:-"3600"} ARMADA_API_TIMEOUT=${ARMADA_API_TIMEOUT:-"3600"}
# Number of uWSGI workers to handle API requests # Number of uWSGI workers to handle API requests