Make Deckhand validation exceptions adhere to UCP standard
This PS makes Deckhand raise an exception formatted including the list ValidationMessage-formatted error messages following any validation error. This adheres to the format specified under [0]. To accomplish this, logic was added to raise an exception with a status code corresponding to the `code` attribute for each DeckhandException subclass. This means it is no longer necessary to raise a specific falcon exception as the process has been automated. In addition, the 'reason' key in the UCP error exception message is now populated if specified for any DeckhandException instance. The same is true for 'error_list'. TODO (in a follow up): * Allow 'info_list' to specified for any DeckhandException instance. * Pass the 'reason' and 'error_list' and etc. arguments to all instances of DeckhandException that are raised. [0] https://github.com/att-comdev/ucp-integration/blob/master/docs/source/api-conventions.rst#output-structure Change-Id: I0cc2909f515ace762be805288981224fc5098c9c
This commit is contained in:
parent
4d3f8b5dcd
commit
e65710bf1a
62
deckhand/common/validation_message.py
Normal file
62
deckhand/common/validation_message.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationMessage(object):
|
||||||
|
"""ValidationMessage per UCP convention:
|
||||||
|
https://github.com/att-comdev/ucp-integration/blob/master/docs/source/api-conventions.rst#output-structure # noqa
|
||||||
|
|
||||||
|
Construction of ``ValidationMessage`` message:
|
||||||
|
|
||||||
|
:param string message: Validation failure message.
|
||||||
|
:param boolean error: True or False, if this is an error message.
|
||||||
|
:param string name: Identifying name of the validation.
|
||||||
|
:param string level: The severity of validation result, as "Error",
|
||||||
|
"Warning", or "Info"
|
||||||
|
:param string schema: The schema of the document being validated.
|
||||||
|
:param string doc_name: The name of the document being validated.
|
||||||
|
:param string diagnostic: Information about what lead to the message,
|
||||||
|
or details for resolution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
message='Document validation error.',
|
||||||
|
error=True,
|
||||||
|
name='Deckhand validation error',
|
||||||
|
level='Error',
|
||||||
|
doc_schema='',
|
||||||
|
doc_name='',
|
||||||
|
doc_layer='',
|
||||||
|
diagnostic=''):
|
||||||
|
level = 'Error' if error else 'Info'
|
||||||
|
self._output = {
|
||||||
|
'message': message,
|
||||||
|
'error': error,
|
||||||
|
'name': name,
|
||||||
|
'documents': [],
|
||||||
|
'level': level,
|
||||||
|
'kind': self.__class__.__name__
|
||||||
|
}
|
||||||
|
self._output['documents'].append(
|
||||||
|
dict(schema=doc_schema, name=doc_name, layer=doc_layer))
|
||||||
|
if diagnostic:
|
||||||
|
self._output.update(diagnostic=diagnostic)
|
||||||
|
|
||||||
|
def format_message(self):
|
||||||
|
"""Return ``ValidationMessage`` message.
|
||||||
|
|
||||||
|
:returns: The ``ValidationMessage`` for the Validation API response.
|
||||||
|
:rtype: dict
|
||||||
|
"""
|
||||||
|
return self._output
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import six
|
from oslo_utils import excutils
|
||||||
|
|
||||||
from deckhand.control import base as api_base
|
from deckhand.control import base as api_base
|
||||||
from deckhand.control.views import document as document_view
|
from deckhand.control.views import document as document_view
|
||||||
@ -48,8 +48,8 @@ class BucketsResource(api_base.BaseResource):
|
|||||||
documents, data_schemas, pre_validate=True)
|
documents, data_schemas, pre_validate=True)
|
||||||
validations = doc_validator.validate_all()
|
validations = doc_validator.validate_all()
|
||||||
except deckhand_errors.InvalidDocumentFormat as e:
|
except deckhand_errors.InvalidDocumentFormat as e:
|
||||||
LOG.exception(e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
raise falcon.HTTPBadRequest(description=e.format_message())
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
for document in documents:
|
for document in documents:
|
||||||
if secrets_manager.SecretsManager.requires_encryption(document):
|
if secrets_manager.SecretsManager.requires_encryption(document):
|
||||||
@ -59,11 +59,10 @@ class BucketsResource(api_base.BaseResource):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
documents = self._prepare_secret_documents(documents)
|
documents = self._prepare_secret_documents(documents)
|
||||||
except deckhand_errors.BarbicanException as e:
|
except deckhand_errors.BarbicanException:
|
||||||
LOG.error('An unknown exception occurred while trying to store '
|
with excutils.save_and_reraise_exception():
|
||||||
'a secret in Barbican.')
|
LOG.error('An unknown exception occurred while trying to store'
|
||||||
raise falcon.HTTPInternalServerError(
|
' a secret in Barbican.')
|
||||||
description=e.format_message())
|
|
||||||
|
|
||||||
created_documents = self._create_revision_documents(
|
created_documents = self._create_revision_documents(
|
||||||
bucket_name, documents, validations)
|
bucket_name, documents, validations)
|
||||||
@ -86,8 +85,7 @@ class BucketsResource(api_base.BaseResource):
|
|||||||
bucket_name, documents, validations=validations)
|
bucket_name, documents, validations=validations)
|
||||||
except (deckhand_errors.DuplicateDocumentExists,
|
except (deckhand_errors.DuplicateDocumentExists,
|
||||||
deckhand_errors.SingletonDocumentConflict) as e:
|
deckhand_errors.SingletonDocumentConflict) as e:
|
||||||
raise falcon.HTTPConflict(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
except Exception as e:
|
LOG.exception(e.format_message())
|
||||||
raise falcon.HTTPInternalServerError(description=six.text_type(e))
|
|
||||||
|
|
||||||
return created_documents
|
return created_documents
|
||||||
|
@ -13,12 +13,16 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import excutils
|
||||||
|
|
||||||
from deckhand.control import base as api_base
|
from deckhand.control import base as api_base
|
||||||
from deckhand.db.sqlalchemy import api as db_api
|
from deckhand.db.sqlalchemy import api as db_api
|
||||||
from deckhand import errors
|
from deckhand import errors
|
||||||
from deckhand import policy
|
from deckhand import policy
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RevisionDiffingResource(api_base.BaseResource):
|
class RevisionDiffingResource(api_base.BaseResource):
|
||||||
"""API resource for realizing revision diffing."""
|
"""API resource for realizing revision diffing."""
|
||||||
@ -33,8 +37,9 @@ class RevisionDiffingResource(api_base.BaseResource):
|
|||||||
try:
|
try:
|
||||||
resp_body = db_api.revision_diff(
|
resp_body = db_api.revision_diff(
|
||||||
revision_id, comparison_revision_id)
|
revision_id, comparison_revision_id)
|
||||||
except (errors.RevisionNotFound) as e:
|
except errors.RevisionNotFound as e:
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
resp.body = resp_body
|
resp.body = resp_body
|
||||||
|
@ -14,9 +14,11 @@
|
|||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import excutils
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from deckhand.common import utils
|
from deckhand.common import utils
|
||||||
|
from deckhand.common.validation_message import ValidationMessage
|
||||||
from deckhand.control import base as api_base
|
from deckhand.control import base as api_base
|
||||||
from deckhand.control import common
|
from deckhand.control import common
|
||||||
from deckhand.control.views import document as document_view
|
from deckhand.control.views import document as document_view
|
||||||
@ -118,17 +120,14 @@ class RenderedDocumentsResource(api_base.BaseResource):
|
|||||||
errors.InvalidDocumentParent,
|
errors.InvalidDocumentParent,
|
||||||
errors.InvalidDocumentReplacement,
|
errors.InvalidDocumentReplacement,
|
||||||
errors.IndeterminateDocumentParent,
|
errors.IndeterminateDocumentParent,
|
||||||
|
errors.LayeringPolicyNotFound,
|
||||||
errors.MissingDocumentKey,
|
errors.MissingDocumentKey,
|
||||||
errors.SubstitutionSourceDataNotFound,
|
errors.SubstitutionSourceDataNotFound,
|
||||||
|
errors.SubstitutionSourceNotFound,
|
||||||
|
errors.UnknownSubstitutionError,
|
||||||
errors.UnsupportedActionMethod) as e:
|
errors.UnsupportedActionMethod) as e:
|
||||||
raise falcon.HTTPBadRequest(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
except (errors.LayeringPolicyNotFound,
|
LOG.exception(e.format_message())
|
||||||
errors.SubstitutionSourceNotFound) as e:
|
|
||||||
raise falcon.HTTPConflict(description=e.format_message())
|
|
||||||
except (errors.DeckhandException,
|
|
||||||
errors.UnknownSubstitutionError) as e:
|
|
||||||
raise falcon.HTTPInternalServerError(
|
|
||||||
description=e.format_message())
|
|
||||||
|
|
||||||
# Filters to be applied post-rendering, because many documents are
|
# Filters to be applied post-rendering, because many documents are
|
||||||
# involved in rendering. User filters can only be applied once all
|
# involved in rendering. User filters can only be applied once all
|
||||||
@ -187,12 +186,37 @@ class RenderedDocumentsResource(api_base.BaseResource):
|
|||||||
try:
|
try:
|
||||||
validations = doc_validator.validate_all()
|
validations = doc_validator.validate_all()
|
||||||
except errors.InvalidDocumentFormat as e:
|
except errors.InvalidDocumentFormat as e:
|
||||||
LOG.error('Failed to post-validate rendered documents.')
|
with excutils.save_and_reraise_exception():
|
||||||
LOG.exception(e.format_message())
|
# Post-rendering validation errors likely indicate an internal
|
||||||
raise falcon.HTTPInternalServerError(
|
# rendering bug, so override the default code to 500.
|
||||||
description=e.format_message())
|
e.code = 500
|
||||||
|
LOG.error('Failed to post-validate rendered documents.')
|
||||||
|
LOG.exception(e.format_message())
|
||||||
else:
|
else:
|
||||||
failed_validations = [
|
error_list = []
|
||||||
v for v in validations if v['status'] == 'failure']
|
|
||||||
if failed_validations:
|
for validation in validations:
|
||||||
raise falcon.HTTPBadRequest(description=failed_validations)
|
if validation['status'] == 'failure':
|
||||||
|
error_list.extend([
|
||||||
|
ValidationMessage(
|
||||||
|
error=True,
|
||||||
|
message=error['message'],
|
||||||
|
doc_schema=error['schema'],
|
||||||
|
doc_name=error['name'],
|
||||||
|
doc_layer=error['layer'],
|
||||||
|
diagnostic={
|
||||||
|
k: v for k, v in error.items() if k in (
|
||||||
|
'schema_path',
|
||||||
|
'validation_schema',
|
||||||
|
'error_section'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for error in validation['errors']
|
||||||
|
])
|
||||||
|
|
||||||
|
if error_list:
|
||||||
|
raise errors.InvalidDocumentFormat(
|
||||||
|
error_list=error_list,
|
||||||
|
reason='Validation'
|
||||||
|
)
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import excutils
|
||||||
|
|
||||||
from deckhand.control import base as api_base
|
from deckhand.control import base as api_base
|
||||||
from deckhand.control.views import revision_tag as revision_tag_view
|
from deckhand.control.views import revision_tag as revision_tag_view
|
||||||
@ -34,10 +35,11 @@ class RevisionTagsResource(api_base.BaseResource):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
resp_tag = db_api.revision_tag_create(revision_id, tag, tag_data)
|
resp_tag = db_api.revision_tag_create(revision_id, tag, tag_data)
|
||||||
except (errors.RevisionNotFound, errors.RevisionTagNotFound) as e:
|
except (errors.RevisionNotFound,
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
errors.RevisionTagBadFormat,
|
||||||
except errors.RevisionTagBadFormat as e:
|
errors.errors.RevisionTagNotFound) as e:
|
||||||
raise falcon.HTTPBadRequest(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
resp_body = revision_tag_view.ViewBuilder().show(resp_tag)
|
resp_body = revision_tag_view.ViewBuilder().show(resp_tag)
|
||||||
resp.status = falcon.HTTP_201
|
resp.status = falcon.HTTP_201
|
||||||
@ -57,7 +59,8 @@ class RevisionTagsResource(api_base.BaseResource):
|
|||||||
resp_tag = db_api.revision_tag_get(revision_id, tag)
|
resp_tag = db_api.revision_tag_get(revision_id, tag)
|
||||||
except (errors.RevisionNotFound,
|
except (errors.RevisionNotFound,
|
||||||
errors.RevisionTagNotFound) as e:
|
errors.RevisionTagNotFound) as e:
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
resp_body = revision_tag_view.ViewBuilder().show(resp_tag)
|
resp_body = revision_tag_view.ViewBuilder().show(resp_tag)
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
@ -69,7 +72,8 @@ class RevisionTagsResource(api_base.BaseResource):
|
|||||||
try:
|
try:
|
||||||
resp_tags = db_api.revision_tag_get_all(revision_id)
|
resp_tags = db_api.revision_tag_get_all(revision_id)
|
||||||
except errors.RevisionNotFound as e:
|
except errors.RevisionNotFound as e:
|
||||||
raise falcon.HTTPNotFound(e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
resp_body = revision_tag_view.ViewBuilder().list(resp_tags)
|
resp_body = revision_tag_view.ViewBuilder().list(resp_tags)
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
@ -89,7 +93,8 @@ class RevisionTagsResource(api_base.BaseResource):
|
|||||||
db_api.revision_tag_delete(revision_id, tag)
|
db_api.revision_tag_delete(revision_id, tag)
|
||||||
except (errors.RevisionNotFound,
|
except (errors.RevisionNotFound,
|
||||||
errors.RevisionTagNotFound) as e:
|
errors.RevisionTagNotFound) as e:
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
resp.status = falcon.HTTP_204
|
resp.status = falcon.HTTP_204
|
||||||
|
|
||||||
@ -99,6 +104,7 @@ class RevisionTagsResource(api_base.BaseResource):
|
|||||||
try:
|
try:
|
||||||
db_api.revision_tag_delete_all(revision_id)
|
db_api.revision_tag_delete_all(revision_id)
|
||||||
except errors.RevisionNotFound as e:
|
except errors.RevisionNotFound as e:
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
resp.status = falcon.HTTP_204
|
resp.status = falcon.HTTP_204
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import excutils
|
||||||
|
|
||||||
from deckhand.common import utils
|
from deckhand.common import utils
|
||||||
from deckhand.control import base as api_base
|
from deckhand.control import base as api_base
|
||||||
@ -22,6 +24,8 @@ from deckhand.db.sqlalchemy import api as db_api
|
|||||||
from deckhand import errors
|
from deckhand import errors
|
||||||
from deckhand import policy
|
from deckhand import policy
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RevisionsResource(api_base.BaseResource):
|
class RevisionsResource(api_base.BaseResource):
|
||||||
"""API resource for realizing CRUD operations for revisions."""
|
"""API resource for realizing CRUD operations for revisions."""
|
||||||
@ -50,7 +54,8 @@ class RevisionsResource(api_base.BaseResource):
|
|||||||
try:
|
try:
|
||||||
revision = db_api.revision_get(revision_id)
|
revision = db_api.revision_get(revision_id)
|
||||||
except errors.RevisionNotFound as e:
|
except errors.RevisionNotFound as e:
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
revision_resp = self.view_builder.show(revision)
|
revision_resp = self.view_builder.show(revision)
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import excutils
|
||||||
|
|
||||||
from deckhand.control import base as api_base
|
from deckhand.control import base as api_base
|
||||||
from deckhand.control.views import revision as revision_view
|
from deckhand.control.views import revision as revision_view
|
||||||
@ -20,6 +22,8 @@ from deckhand.db.sqlalchemy import api as db_api
|
|||||||
from deckhand import errors
|
from deckhand import errors
|
||||||
from deckhand import policy
|
from deckhand import policy
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RollbackResource(api_base.BaseResource):
|
class RollbackResource(api_base.BaseResource):
|
||||||
"""API resource for realizing revision rollback."""
|
"""API resource for realizing revision rollback."""
|
||||||
@ -31,7 +35,8 @@ class RollbackResource(api_base.BaseResource):
|
|||||||
try:
|
try:
|
||||||
latest_revision = db_api.revision_get_latest()
|
latest_revision = db_api.revision_get_latest()
|
||||||
except errors.RevisionNotFound as e:
|
except errors.RevisionNotFound as e:
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
for document in latest_revision['documents']:
|
for document in latest_revision['documents']:
|
||||||
if document['metadata'].get('storagePolicy') == 'encrypted':
|
if document['metadata'].get('storagePolicy') == 'encrypted':
|
||||||
@ -43,7 +48,8 @@ class RollbackResource(api_base.BaseResource):
|
|||||||
rollback_revision = db_api.revision_rollback(
|
rollback_revision = db_api.revision_rollback(
|
||||||
revision_id, latest_revision)
|
revision_id, latest_revision)
|
||||||
except errors.InvalidRollback as e:
|
except errors.InvalidRollback as e:
|
||||||
raise falcon.HTTPBadRequest(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
revision_resp = self.view_builder.show(rollback_revision)
|
revision_resp = self.view_builder.show(rollback_revision)
|
||||||
resp.status = falcon.HTTP_201
|
resp.status = falcon.HTTP_201
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import excutils
|
||||||
|
|
||||||
from deckhand.control import base as api_base
|
from deckhand.control import base as api_base
|
||||||
from deckhand.control.views import validation as validation_view
|
from deckhand.control.views import validation as validation_view
|
||||||
@ -44,7 +45,8 @@ class ValidationsResource(api_base.BaseResource):
|
|||||||
resp_body = db_api.validation_create(
|
resp_body = db_api.validation_create(
|
||||||
revision_id, validation_name, validation_data)
|
revision_id, validation_name, validation_data)
|
||||||
except errors.RevisionNotFound as e:
|
except errors.RevisionNotFound as e:
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
resp.status = falcon.HTTP_201
|
resp.status = falcon.HTTP_201
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
resp.append_header('Content-Type', 'application/x-yaml')
|
||||||
@ -77,8 +79,10 @@ class ValidationsResource(api_base.BaseResource):
|
|||||||
try:
|
try:
|
||||||
entry = db_api.validation_get_entry(
|
entry = db_api.validation_get_entry(
|
||||||
revision_id, validation_name, entry_id)
|
revision_id, validation_name, entry_id)
|
||||||
except (errors.RevisionNotFound, errors.ValidationNotFound) as e:
|
except (errors.RevisionNotFound,
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
errors.ValidationNotFound) as e:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
resp_body = self.view_builder.show_entry(entry)
|
resp_body = self.view_builder.show_entry(entry)
|
||||||
return resp_body
|
return resp_body
|
||||||
@ -90,7 +94,8 @@ class ValidationsResource(api_base.BaseResource):
|
|||||||
entries = db_api.validation_get_all_entries(revision_id,
|
entries = db_api.validation_get_all_entries(revision_id,
|
||||||
validation_name)
|
validation_name)
|
||||||
except errors.RevisionNotFound as e:
|
except errors.RevisionNotFound as e:
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
resp_body = self.view_builder.list_entries(entries)
|
resp_body = self.view_builder.list_entries(entries)
|
||||||
return resp_body
|
return resp_body
|
||||||
@ -100,7 +105,8 @@ class ValidationsResource(api_base.BaseResource):
|
|||||||
try:
|
try:
|
||||||
validations = db_api.validation_get_all(revision_id)
|
validations = db_api.validation_get_all(revision_id)
|
||||||
except errors.RevisionNotFound as e:
|
except errors.RevisionNotFound as e:
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.exception(e.format_message())
|
||||||
|
|
||||||
resp_body = self.view_builder.list(validations)
|
resp_body = self.view_builder.list(validations)
|
||||||
return resp_body
|
return resp_body
|
||||||
|
@ -35,14 +35,14 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
attrs = ['id', 'metadata', 'data', 'schema']
|
attrs = ['id', 'metadata', 'data', 'schema']
|
||||||
|
|
||||||
for document in documents:
|
for document in documents:
|
||||||
if document['deleted']:
|
if document.get('deleted'):
|
||||||
continue
|
continue
|
||||||
if document['schema'].startswith(types.VALIDATION_POLICY_SCHEMA):
|
if document['schema'].startswith(types.VALIDATION_POLICY_SCHEMA):
|
||||||
continue
|
continue
|
||||||
resp_obj = {x: document[x] for x in attrs}
|
resp_obj = {x: document.get(x) for x in attrs}
|
||||||
resp_obj.setdefault('status', {})
|
resp_obj.setdefault('status', {})
|
||||||
resp_obj['status']['bucket'] = document['bucket_name']
|
resp_obj['status']['bucket'] = document.get('bucket_name')
|
||||||
resp_obj['status']['revision'] = document['revision_id']
|
resp_obj['status']['revision'] = document.get('revision_id')
|
||||||
resp_list.append(resp_obj)
|
resp_list.append(resp_obj)
|
||||||
|
|
||||||
# Edge case for when all documents are deleted from a bucket. To detect
|
# Edge case for when all documents are deleted from a bucket. To detect
|
||||||
@ -53,8 +53,8 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
# across all the documents in ``documents``.
|
# across all the documents in ``documents``.
|
||||||
if not resp_list and documents:
|
if not resp_list and documents:
|
||||||
resp_obj = {'status': {}}
|
resp_obj = {'status': {}}
|
||||||
resp_obj['status']['bucket'] = documents[0]['bucket_name']
|
resp_obj['status']['bucket'] = documents[0].get('bucket_name')
|
||||||
resp_obj['status']['revision'] = documents[0]['revision_id']
|
resp_obj['status']['revision'] = documents[0].get('revision_id')
|
||||||
return [resp_obj]
|
return [resp_obj]
|
||||||
|
|
||||||
return resp_list
|
return resp_list
|
||||||
|
@ -25,6 +25,7 @@ import six
|
|||||||
|
|
||||||
from deckhand.common import document as document_wrapper
|
from deckhand.common import document as document_wrapper
|
||||||
from deckhand.common import utils
|
from deckhand.common import utils
|
||||||
|
from deckhand.common.validation_message import ValidationMessage
|
||||||
from deckhand.engine.secrets_manager import SecretsSubstitution
|
from deckhand.engine.secrets_manager import SecretsSubstitution
|
||||||
from deckhand import errors
|
from deckhand import errors
|
||||||
from deckhand import types
|
from deckhand import types
|
||||||
@ -107,6 +108,11 @@ class GenericValidator(BaseValidator):
|
|||||||
|
|
||||||
__slots__ = ('base_schema')
|
__slots__ = ('base_schema')
|
||||||
|
|
||||||
|
_diagnostic = (
|
||||||
|
'Ensure that each document has a metadata, schema and data section. '
|
||||||
|
'Each document must pass the schema defined under: '
|
||||||
|
'http://deckhand.readthedocs.io/en/latest/validation.html#base-schema')
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(GenericValidator, self).__init__()
|
super(GenericValidator, self).__init__()
|
||||||
self.base_schema = self._schema_map['v1']['deckhand/Base']
|
self.base_schema = self._schema_map['v1']['deckhand/Base']
|
||||||
@ -149,8 +155,16 @@ class GenericValidator(BaseValidator):
|
|||||||
'Details: %s', document.schema, document.layer,
|
'Details: %s', document.schema, document.layer,
|
||||||
document.name, error_messages)
|
document.name, error_messages)
|
||||||
raise errors.InvalidDocumentFormat(
|
raise errors.InvalidDocumentFormat(
|
||||||
schema=document.schema, name=document.name,
|
error_list=[
|
||||||
layer=document.layer, errors=', '.join(error_messages))
|
ValidationMessage(message=message,
|
||||||
|
doc_schema=document.schema,
|
||||||
|
doc_name=document.name,
|
||||||
|
doc_layer=document.layer,
|
||||||
|
diagnostic=self._diagnostic)
|
||||||
|
for message in error_messages
|
||||||
|
],
|
||||||
|
reason='Validation'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DataSchemaValidator(GenericValidator):
|
class DataSchemaValidator(GenericValidator):
|
||||||
@ -430,6 +444,7 @@ class DocumentValidation(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatted_results = []
|
formatted_results = []
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
formatted_result = {
|
formatted_result = {
|
||||||
'name': types.DECKHAND_SCHEMA_VALIDATION,
|
'name': types.DECKHAND_SCHEMA_VALIDATION,
|
||||||
@ -459,8 +474,7 @@ class DocumentValidation(object):
|
|||||||
error_outputs = validator.validate(
|
error_outputs = validator.validate(
|
||||||
document, pre_validate=self._pre_validate)
|
document, pre_validate=self._pre_validate)
|
||||||
if error_outputs:
|
if error_outputs:
|
||||||
for error_output in error_outputs:
|
result['errors'].extend(error_outputs)
|
||||||
result['errors'].append(error_output)
|
|
||||||
|
|
||||||
if result['errors']:
|
if result['errors']:
|
||||||
result.setdefault('status', 'failure')
|
result.setdefault('status', 'failure')
|
||||||
@ -503,5 +517,4 @@ class DocumentValidation(object):
|
|||||||
result = self._validate_one(document)
|
result = self._validate_one(document)
|
||||||
validation_results.append(result)
|
validation_results.append(result)
|
||||||
|
|
||||||
validations = self._format_validation_results(validation_results)
|
return self._format_validation_results(validation_results)
|
||||||
return validations
|
|
||||||
|
@ -24,6 +24,7 @@ from oslo_utils import excutils
|
|||||||
|
|
||||||
from deckhand.common import document as document_wrapper
|
from deckhand.common import document as document_wrapper
|
||||||
from deckhand.common import utils
|
from deckhand.common import utils
|
||||||
|
from deckhand.common.validation_message import ValidationMessage
|
||||||
from deckhand.engine import document_validation
|
from deckhand.engine import document_validation
|
||||||
from deckhand.engine import secrets_manager
|
from deckhand.engine import secrets_manager
|
||||||
from deckhand.engine import utils as engine_utils
|
from deckhand.engine import utils as engine_utils
|
||||||
@ -343,21 +344,24 @@ class DocumentLayering(object):
|
|||||||
validator = document_validation.DocumentValidation(
|
validator = document_validation.DocumentValidation(
|
||||||
documents, pre_validate=True)
|
documents, pre_validate=True)
|
||||||
results = validator.validate_all()
|
results = validator.validate_all()
|
||||||
val_errors = []
|
|
||||||
|
error_list = []
|
||||||
for result in results:
|
for result in results:
|
||||||
val_errors.extend(
|
for e in result['errors']:
|
||||||
[(e['schema'], e['layer'], e['name'], e['message'])
|
LOG.error('Document [%s, %s] %s failed with pre-validation '
|
||||||
for e in result['errors']])
|
'error: %s.', e['schema'], e['layer'], e['name'],
|
||||||
if val_errors:
|
e['message'])
|
||||||
for error in val_errors:
|
error_list.append(
|
||||||
LOG.error(
|
ValidationMessage(
|
||||||
'Document [%s, %s] %s failed with pre-validation error: '
|
message=e['message'],
|
||||||
'%s.', *error)
|
doc_schema=e['schema'],
|
||||||
raise errors.InvalidDocumentFormat(
|
doc_name=e['name'],
|
||||||
schema=', '.join(v[0] for v in val_errors),
|
doc_layer=e['layer']
|
||||||
layer=', '.join(v[1] for v in val_errors),
|
)
|
||||||
name=', '.join(v[2] for v in val_errors),
|
)
|
||||||
errors=', '.join(v[3] for v in val_errors))
|
|
||||||
|
if error_list:
|
||||||
|
raise errors.InvalidDocumentFormat(error_list=error_list)
|
||||||
|
|
||||||
def __init__(self, documents, substitution_sources=None, validate=True,
|
def __init__(self, documents, substitution_sources=None, validate=True,
|
||||||
fail_on_missing_sub_src=True):
|
fail_on_missing_sub_src=True):
|
||||||
@ -536,8 +540,10 @@ class DocumentLayering(object):
|
|||||||
if from_child is None:
|
if from_child is None:
|
||||||
raise errors.MissingDocumentKey(
|
raise errors.MissingDocumentKey(
|
||||||
child_schema=child_data.schema,
|
child_schema=child_data.schema,
|
||||||
|
child_layer=child_data.layer,
|
||||||
child_name=child_data.name,
|
child_name=child_data.name,
|
||||||
parent_schema=overall_data.schema,
|
parent_schema=overall_data.schema,
|
||||||
|
parent_layer=overall_data.layer,
|
||||||
parent_name=overall_data.name,
|
parent_name=overall_data.name,
|
||||||
action=action)
|
action=action)
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
# 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 traceback
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
@ -34,7 +33,7 @@ def format_error_resp(req,
|
|||||||
resp,
|
resp,
|
||||||
status_code=falcon.HTTP_500,
|
status_code=falcon.HTTP_500,
|
||||||
message="",
|
message="",
|
||||||
reason="",
|
reason=None,
|
||||||
error_type=None,
|
error_type=None,
|
||||||
error_list=None,
|
error_list=None,
|
||||||
info_list=None):
|
info_list=None):
|
||||||
@ -63,21 +62,20 @@ def format_error_resp(req,
|
|||||||
'error': ``False`` field.
|
'error': ``False`` field.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if error_type is None:
|
error_type = error_type or 'Unspecified Exception'
|
||||||
error_type = 'Unspecified Exception'
|
reason = reason or 'Unspecified'
|
||||||
|
|
||||||
# Since we're handling errors here, if error list is None, set up a default
|
# Since we're handling errors here, if error list is None, set up a default
|
||||||
# error item. If we have info items, add them to the message list as well.
|
# error item. If we have info items, add them to the message list as well.
|
||||||
# In both cases, if the error flag is not set, set it appropriately.
|
# In both cases, if the error flag is not set, set it appropriately.
|
||||||
if error_list is None:
|
if not error_list:
|
||||||
error_list = [{'message': 'An error occurred, but was not specified',
|
error_list = [{'message': message, 'error': True}]
|
||||||
'error': True}]
|
|
||||||
else:
|
else:
|
||||||
for error_item in error_list:
|
for error_item in error_list:
|
||||||
if 'error' not in error_item:
|
if 'error' not in error_item:
|
||||||
error_item['error'] = True
|
error_item['error'] = True
|
||||||
|
|
||||||
if info_list is None:
|
if not info_list:
|
||||||
info_list = []
|
info_list = []
|
||||||
else:
|
else:
|
||||||
for info_item in info_list:
|
for info_item in info_list:
|
||||||
@ -87,7 +85,7 @@ def format_error_resp(req,
|
|||||||
message_list = error_list + info_list
|
message_list = error_list + info_list
|
||||||
|
|
||||||
error_response = {
|
error_response = {
|
||||||
'kind': 'status',
|
'kind': 'Status',
|
||||||
'apiVersion': get_version_from_request(req),
|
'apiVersion': get_version_from_request(req),
|
||||||
'metadata': {},
|
'metadata': {},
|
||||||
'status': 'Failure',
|
'status': 'Failure',
|
||||||
@ -104,23 +102,35 @@ def format_error_resp(req,
|
|||||||
'retry': True if status_code is falcon.HTTP_500 else False
|
'retry': True if status_code is falcon.HTTP_500 else False
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.body = yaml.safe_dump(error_response)
|
# Don't use yaml.safe_dump to handle unicode correctly.
|
||||||
|
resp.body = yaml.dump(error_response)
|
||||||
resp.status = status_code
|
resp.status = status_code
|
||||||
|
|
||||||
|
|
||||||
def default_exception_handler(ex, req, resp, params):
|
def default_exception_handler(ex, req, resp, params):
|
||||||
"""Catch-all execption handler for standardized output.
|
"""Catch-all exception handler for standardized output.
|
||||||
|
|
||||||
If this is a standard falcon HTTPError, rethrow it for handling by
|
If this is a standard falcon HTTPError, rethrow it for handling by
|
||||||
``default_exception_serializer`` below.
|
``default_exception_serializer`` below.
|
||||||
"""
|
"""
|
||||||
if isinstance(ex, falcon.HTTPError):
|
if isinstance(ex, falcon.HTTPError):
|
||||||
# Allow the falcon http errors to bubble up and get handled.
|
# Allow the falcon HTTP errors to bubble up and get handled.
|
||||||
raise ex
|
raise ex
|
||||||
|
elif isinstance(ex, DeckhandException):
|
||||||
|
status_code = (getattr(falcon, 'HTTP_%d' % ex.code, falcon.HTTP_500)
|
||||||
|
if hasattr(ex, 'code') else falcon.HTTP_500)
|
||||||
|
|
||||||
|
format_error_resp(
|
||||||
|
req,
|
||||||
|
resp,
|
||||||
|
status_code=status_code,
|
||||||
|
message=ex.message,
|
||||||
|
error_type=ex.__class__.__name__,
|
||||||
|
error_list=getattr(ex, 'error_list', None),
|
||||||
|
reason=getattr(ex, 'reason', None)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Take care of the uncaught stuff.
|
# Take care of the uncaught stuff.
|
||||||
exc_string = traceback.format_exc()
|
|
||||||
LOG.error('Unhanded Exception being handled: \n%s', exc_string)
|
|
||||||
format_error_resp(
|
format_error_resp(
|
||||||
req,
|
req,
|
||||||
resp,
|
resp,
|
||||||
@ -139,9 +149,9 @@ def default_exception_serializer(req, resp, exception):
|
|||||||
status_code=exception.status,
|
status_code=exception.status,
|
||||||
# TODO(fmontei): Provide an overall error message instead.
|
# TODO(fmontei): Provide an overall error message instead.
|
||||||
message=exception.description,
|
message=exception.description,
|
||||||
reason=exception.title,
|
|
||||||
error_type=exception.__class__.__name__,
|
error_type=exception.__class__.__name__,
|
||||||
error_list=[{'message': exception.description, 'error': True}]
|
error_list=getattr(exception, 'error_list', None),
|
||||||
|
reason=getattr(exception, 'reason', None)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -164,6 +174,18 @@ class DeckhandException(Exception):
|
|||||||
message = self.msg_fmt
|
message = self.msg_fmt
|
||||||
|
|
||||||
self.message = message
|
self.message = message
|
||||||
|
self.reason = kwargs.pop('reason', None)
|
||||||
|
|
||||||
|
error_list = kwargs.pop('error_list', [])
|
||||||
|
self.error_list = []
|
||||||
|
|
||||||
|
for error in error_list:
|
||||||
|
if isinstance(error, str):
|
||||||
|
error = {'message': error, 'error': True}
|
||||||
|
else:
|
||||||
|
error = error.format_message()
|
||||||
|
self.error_list.append(error)
|
||||||
|
|
||||||
super(DeckhandException, self).__init__(message)
|
super(DeckhandException, self).__init__(message)
|
||||||
|
|
||||||
def format_message(self):
|
def format_message(self):
|
||||||
@ -175,8 +197,7 @@ class InvalidDocumentFormat(DeckhandException):
|
|||||||
|
|
||||||
**Troubleshoot:**
|
**Troubleshoot:**
|
||||||
"""
|
"""
|
||||||
msg_fmt = ("The provided document(s) schema=%(schema)s, layer=%(layer)s, "
|
msg_fmt = ("The provided documents failed schema validation.")
|
||||||
"name=%(name)s failed schema validation. Errors: %(errors)s")
|
|
||||||
code = 400
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
@ -206,6 +227,7 @@ class InvalidDocumentParent(DeckhandException):
|
|||||||
msg_fmt = ("The document parent [%(parent_schema)s] %(parent_name)s is "
|
msg_fmt = ("The document parent [%(parent_schema)s] %(parent_name)s is "
|
||||||
"invalid for document [%(document_schema)s] %(document_name)s. "
|
"invalid for document [%(document_schema)s] %(document_name)s. "
|
||||||
"Reason: %(reason)s")
|
"Reason: %(reason)s")
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
class IndeterminateDocumentParent(DeckhandException):
|
class IndeterminateDocumentParent(DeckhandException):
|
||||||
|
@ -145,37 +145,22 @@ tests:
|
|||||||
status: 400
|
status: 400
|
||||||
response_multidoc_jsonpaths:
|
response_multidoc_jsonpaths:
|
||||||
$.`len`: 1
|
$.`len`: 1
|
||||||
|
$.[0].apiVersion: v1.0
|
||||||
|
$.[0].code: 400 Bad Request
|
||||||
|
$.[0].details.errorCount: 1
|
||||||
|
$.[0].details.errorType: InvalidDocumentFormat
|
||||||
|
$.[0].details.messageList[0].documents:
|
||||||
|
- layer: site
|
||||||
|
name: bad
|
||||||
|
schema: example/Doc/v1
|
||||||
|
$.[0].details.messageList[0].error: true
|
||||||
|
$.[0].details.messageList[0].kind: ValidationMessage
|
||||||
|
$.[0].details.messageList[0].level: Error
|
||||||
|
$.[0].details.messageList[0].name: Deckhand validation error
|
||||||
|
$.[0].kind: Status
|
||||||
|
$.[0].message: The provided documents failed schema validation.
|
||||||
|
$.[0].reason: Validation
|
||||||
$.[0].status: Failure
|
$.[0].status: Failure
|
||||||
$.[0].message:
|
|
||||||
- errors:
|
|
||||||
- validation_schema:
|
|
||||||
"$schema": http://json-schema.org/schema#
|
|
||||||
properties:
|
|
||||||
a:
|
|
||||||
type: string
|
|
||||||
b:
|
|
||||||
maximum: 100
|
|
||||||
type: integer
|
|
||||||
minimum: 0
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- a
|
|
||||||
- b
|
|
||||||
additionalProperties: false
|
|
||||||
error_section:
|
|
||||||
a: this-one-is-required-and-can-be-different
|
|
||||||
b: 177
|
|
||||||
schema_path: ".properties.b.maximum"
|
|
||||||
name: bad
|
|
||||||
schema: example/Doc/v1
|
|
||||||
layer: site
|
|
||||||
path: ".data.b"
|
|
||||||
message: 177 is greater than the maximum of 100
|
|
||||||
name: deckhand-schema-validation
|
|
||||||
validator:
|
|
||||||
name: deckhand
|
|
||||||
version: '1.0'
|
|
||||||
status: failure
|
|
||||||
|
|
||||||
- name: add_invalid_document_with_substitutions
|
- name: add_invalid_document_with_substitutions
|
||||||
desc: Add a document that does not follow the schema
|
desc: Add a document that does not follow the schema
|
||||||
@ -216,34 +201,9 @@ tests:
|
|||||||
status: 400
|
status: 400
|
||||||
response_multidoc_jsonpaths:
|
response_multidoc_jsonpaths:
|
||||||
$.`len`: 1
|
$.`len`: 1
|
||||||
$.[0].status: Failure
|
$.[0].code: 400 Bad Request
|
||||||
$.[0].message:
|
$.[0].details.errorCount: 1
|
||||||
- errors:
|
$.[0].details.errorType: InvalidDocumentFormat
|
||||||
- name: bad
|
$.[0].details.messageList[0].diagnostic.error_section:
|
||||||
layer: site
|
a: 'Sanitized to avoid exposing secret.'
|
||||||
schema: example/Doc/v1
|
b: 177
|
||||||
path: .data.b
|
|
||||||
schema_path: .properties.b.maximum
|
|
||||||
error_section:
|
|
||||||
a: Sanitized to avoid exposing secret.
|
|
||||||
b: 177
|
|
||||||
message: 177 is greater than the maximum of 100
|
|
||||||
validation_schema:
|
|
||||||
$schema: http://json-schema.org/schema#
|
|
||||||
additionalProperties: False
|
|
||||||
properties:
|
|
||||||
a:
|
|
||||||
type: string
|
|
||||||
b:
|
|
||||||
maximum: 100
|
|
||||||
minimum: 0
|
|
||||||
type: integer
|
|
||||||
required:
|
|
||||||
- a
|
|
||||||
- b
|
|
||||||
type: object
|
|
||||||
name: deckhand-schema-validation
|
|
||||||
validator:
|
|
||||||
name: deckhand
|
|
||||||
version: '1.0'
|
|
||||||
status: failure
|
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
# 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 os
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
@ -22,44 +23,41 @@ from deckhand.tests.unit.control import base as test_base
|
|||||||
|
|
||||||
|
|
||||||
class TestErrorFormatting(test_base.BaseControllerTest):
|
class TestErrorFormatting(test_base.BaseControllerTest):
|
||||||
"""Test suite for validating error formatting.
|
"""Test suite for validating error formatting."""
|
||||||
|
|
||||||
Use mocked exceptions below to guarantee consistent results.
|
def test_python_exception_formatting(self):
|
||||||
"""
|
|
||||||
|
|
||||||
def test_base_exception_formatting(self):
|
|
||||||
"""Verify formatting for an exception class that inherits from
|
"""Verify formatting for an exception class that inherits from
|
||||||
:class:`Exception`.
|
:class:`Exception`.
|
||||||
"""
|
"""
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
policy, '_do_enforce_rbac',
|
policy, '_do_enforce_rbac',
|
||||||
spec_set=policy._do_enforce_rbac) as m_enforce_rbac:
|
spec_set=policy._do_enforce_rbac) as m_enforce_rbac:
|
||||||
m_enforce_rbac.side_effect = Exception
|
m_enforce_rbac.side_effect = Exception('test error')
|
||||||
resp = self.app.simulate_put(
|
resp = self.app.simulate_put(
|
||||||
'/api/v1.0/buckets/test/documents',
|
'/api/v1.0/buckets/test/documents',
|
||||||
headers={'Content-Type': 'application/x-yaml'}, body=None)
|
headers={'Content-Type': 'application/x-yaml'}, body=None)
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
'status': 'Failure',
|
'status': 'Failure',
|
||||||
'kind': 'status',
|
'kind': 'Status',
|
||||||
'code': '500 Internal Server Error',
|
'code': '500 Internal Server Error',
|
||||||
'apiVersion': 'v1.0',
|
'apiVersion': 'v1.0',
|
||||||
'reason': '',
|
'reason': 'Unspecified',
|
||||||
'retry': True,
|
'retry': True,
|
||||||
'details': {
|
'details': {
|
||||||
'errorType': 'Exception',
|
'errorType': 'Exception',
|
||||||
'errorCount': 1,
|
'errorCount': 1,
|
||||||
'messageList': [
|
'messageList': [
|
||||||
{
|
{
|
||||||
'message': 'An error occurred, but was not specified',
|
'message': 'Unhandled Exception raised: test error',
|
||||||
'error': True
|
'error': True
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
'message': 'Unhandled Exception raised: ',
|
'message': 'Unhandled Exception raised: test error',
|
||||||
'metadata': {}
|
'metadata': {}
|
||||||
}
|
}
|
||||||
body = yaml.safe_load(resp.text)
|
body = yaml.load(resp.text)
|
||||||
|
|
||||||
self.assertEqual(500, resp.status_code)
|
self.assertEqual(500, resp.status_code)
|
||||||
self.assertEqual(expected, body)
|
self.assertEqual(expected, body)
|
||||||
@ -82,10 +80,10 @@ class TestErrorFormatting(test_base.BaseControllerTest):
|
|||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
'status': 'Failure',
|
'status': 'Failure',
|
||||||
'kind': 'status',
|
'kind': 'Status',
|
||||||
'code': '403 Forbidden',
|
'code': '403 Forbidden',
|
||||||
'apiVersion': 'v1.0',
|
'apiVersion': 'v1.0',
|
||||||
'reason': '403 Forbidden',
|
'reason': 'Unspecified',
|
||||||
'retry': False,
|
'retry': False,
|
||||||
'details': {
|
'details': {
|
||||||
'errorType': 'HTTPForbidden',
|
'errorType': 'HTTPForbidden',
|
||||||
@ -104,3 +102,128 @@ class TestErrorFormatting(test_base.BaseControllerTest):
|
|||||||
|
|
||||||
self.assertEqual(403, resp.status_code)
|
self.assertEqual(403, resp.status_code)
|
||||||
self.assertEqual(expected, body)
|
self.assertEqual(expected, body)
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidationMessageFormatting(test_base.BaseControllerTest):
|
||||||
|
"""Test suite for validating :class:`ValidationMessage` formatting."""
|
||||||
|
|
||||||
|
def test_put_bucket_validation_message_formatting(self):
|
||||||
|
"""Verify formatting for pre-validation during updating a bucket."""
|
||||||
|
rules = {'deckhand:create_cleartext_documents': '@'}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
|
resp = self.app.simulate_put(
|
||||||
|
'/api/v1.0/buckets/test/documents',
|
||||||
|
headers={'Content-Type': 'application/x-yaml'},
|
||||||
|
body='name: test')
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'status': 'Failure',
|
||||||
|
'kind': 'Status',
|
||||||
|
'code': '400 Bad Request',
|
||||||
|
'apiVersion': 'v1.0',
|
||||||
|
'reason': 'Validation',
|
||||||
|
'retry': False,
|
||||||
|
'details': {
|
||||||
|
'errorType': 'InvalidDocumentFormat',
|
||||||
|
'errorCount': 2,
|
||||||
|
'messageList': [
|
||||||
|
{
|
||||||
|
'diagnostic': mock.ANY,
|
||||||
|
'documents': [{
|
||||||
|
'layer': None,
|
||||||
|
'name': None,
|
||||||
|
'schema': ''
|
||||||
|
}],
|
||||||
|
'error': True,
|
||||||
|
'kind': 'ValidationMessage',
|
||||||
|
'level': 'Error',
|
||||||
|
'message': mock.ANY,
|
||||||
|
'name': 'Deckhand validation error'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'diagnostic': mock.ANY,
|
||||||
|
'documents': [{
|
||||||
|
'layer': None,
|
||||||
|
'name': None,
|
||||||
|
'schema': ''
|
||||||
|
}],
|
||||||
|
'error': True,
|
||||||
|
'kind': 'ValidationMessage',
|
||||||
|
'level': 'Error',
|
||||||
|
'message': mock.ANY,
|
||||||
|
'name': 'Deckhand validation error'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'message': 'The provided documents failed schema validation.',
|
||||||
|
'metadata': {}
|
||||||
|
}
|
||||||
|
body = yaml.safe_load(resp.text)
|
||||||
|
|
||||||
|
self.assertEqual(400, resp.status_code)
|
||||||
|
self.assertEqual(expected, body)
|
||||||
|
|
||||||
|
def test_rendered_documents_validation_message_formatting(self):
|
||||||
|
"""Verify formatting for post-validation during rendering revision
|
||||||
|
documents.
|
||||||
|
"""
|
||||||
|
rules = {'deckhand:create_cleartext_documents': '@',
|
||||||
|
'deckhand:list_cleartext_documents': '@',
|
||||||
|
'deckhand:list_encrypted_documents': '@'}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
|
yaml_file = os.path.join(os.getcwd(), 'deckhand', 'tests', 'unit',
|
||||||
|
'resources', 'sample_layering_policy.yaml')
|
||||||
|
with open(yaml_file) as yaml_stream:
|
||||||
|
payload = yaml_stream.read()
|
||||||
|
|
||||||
|
resp = self.app.simulate_put(
|
||||||
|
'/api/v1.0/buckets/test/documents',
|
||||||
|
headers={'Content-Type': 'application/x-yaml'},
|
||||||
|
body=payload)
|
||||||
|
|
||||||
|
with mock.patch('deckhand.control.revision_documents.db_api'
|
||||||
|
'.revision_documents_get', autospec=True) \
|
||||||
|
as mock_get_rev_documents:
|
||||||
|
invalid_document = yaml.safe_load(payload)
|
||||||
|
invalid_document.pop('metadata')
|
||||||
|
|
||||||
|
mock_get_rev_documents.return_value = [invalid_document]
|
||||||
|
resp = self.app.simulate_get(
|
||||||
|
'/api/v1.0/revisions/1/rendered-documents',
|
||||||
|
headers={'Content-Type': 'application/x-yaml'})
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'status': 'Failure',
|
||||||
|
'kind': 'Status',
|
||||||
|
'code': '500 Internal Server Error',
|
||||||
|
'apiVersion': 'v1.0',
|
||||||
|
'reason': 'Validation',
|
||||||
|
'retry': True,
|
||||||
|
'details': {
|
||||||
|
'errorType': 'InvalidDocumentFormat',
|
||||||
|
'errorCount': 1,
|
||||||
|
'messageList': [
|
||||||
|
{
|
||||||
|
'diagnostic': mock.ANY,
|
||||||
|
'documents': [{
|
||||||
|
'layer': None,
|
||||||
|
'name': None,
|
||||||
|
'schema': invalid_document['schema']
|
||||||
|
}],
|
||||||
|
'error': True,
|
||||||
|
'kind': 'ValidationMessage',
|
||||||
|
'level': 'Error',
|
||||||
|
'message': mock.ANY,
|
||||||
|
'name': 'Deckhand validation error'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'message': 'The provided documents failed schema validation.',
|
||||||
|
'metadata': {}
|
||||||
|
}
|
||||||
|
body = yaml.safe_load(resp.text)
|
||||||
|
|
||||||
|
self.assertEqual(500, resp.status_code)
|
||||||
|
self.assertEqual(expected, body)
|
||||||
|
@ -121,10 +121,10 @@ class TestYAMLTranslatorNegative(test_base.BaseControllerTest):
|
|||||||
'message': "The Content-Type header is required."
|
'message': "The Content-Type header is required."
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
'kind': 'status',
|
'kind': 'Status',
|
||||||
'message': "The Content-Type header is required.",
|
'message': "The Content-Type header is required.",
|
||||||
'metadata': {},
|
'metadata': {},
|
||||||
'reason': 'Missing header value',
|
'reason': 'Unspecified',
|
||||||
'retry': False,
|
'retry': False,
|
||||||
'status': 'Failure'
|
'status': 'Failure'
|
||||||
}
|
}
|
||||||
@ -153,11 +153,11 @@ class TestYAMLTranslatorNegative(test_base.BaseControllerTest):
|
|||||||
"content types are: ['application/x-yaml'].")
|
"content types are: ['application/x-yaml'].")
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
'kind': 'status',
|
'kind': 'Status',
|
||||||
'message': ("Unexpected content type: application/json. Expected "
|
'message': ("Unexpected content type: application/json. Expected "
|
||||||
"content types are: ['application/x-yaml']."),
|
"content types are: ['application/x-yaml']."),
|
||||||
'metadata': {},
|
'metadata': {},
|
||||||
'reason': 'Unsupported media type',
|
'reason': 'Unspecified',
|
||||||
'retry': False,
|
'retry': False,
|
||||||
'status': 'Failure'
|
'status': 'Failure'
|
||||||
}
|
}
|
||||||
@ -188,11 +188,11 @@ class TestYAMLTranslatorNegative(test_base.BaseControllerTest):
|
|||||||
"content types are: ['application/x-yaml'].")
|
"content types are: ['application/x-yaml'].")
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
'kind': 'status',
|
'kind': 'Status',
|
||||||
'message': ("Unexpected content type: application/yaml. Expected "
|
'message': ("Unexpected content type: application/yaml. Expected "
|
||||||
"content types are: ['application/x-yaml']."),
|
"content types are: ['application/x-yaml']."),
|
||||||
'metadata': {},
|
'metadata': {},
|
||||||
'reason': 'Unsupported media type',
|
'reason': 'Unspecified',
|
||||||
'retry': False,
|
'retry': False,
|
||||||
'status': 'Failure'
|
'status': 'Failure'
|
||||||
}
|
}
|
||||||
|
@ -200,8 +200,8 @@ class TestRenderedDocumentsControllerNegative(
|
|||||||
test_base.BaseControllerTest):
|
test_base.BaseControllerTest):
|
||||||
|
|
||||||
def test_rendered_documents_fail_schema_validation(self):
|
def test_rendered_documents_fail_schema_validation(self):
|
||||||
"""Validates that when fully rendered documents fail schema validation,
|
"""Validates that when fully rendered documents fail basic schema
|
||||||
the controller raises a 500 Internal Server Error.
|
validation (sanity-checking), a 500 is raised.
|
||||||
"""
|
"""
|
||||||
rules = {'deckhand:list_cleartext_documents': '@',
|
rules = {'deckhand:list_cleartext_documents': '@',
|
||||||
'deckhand:list_encrypted_documents': '@',
|
'deckhand:list_encrypted_documents': '@',
|
||||||
@ -232,6 +232,61 @@ class TestRenderedDocumentsControllerNegative(
|
|||||||
# schema validation.
|
# schema validation.
|
||||||
self.assertEqual(500, resp.status_code)
|
self.assertEqual(500, resp.status_code)
|
||||||
|
|
||||||
|
def test_rendered_documents_fail_post_validation(self):
|
||||||
|
"""Validates that when fully rendered documents fail schema validation,
|
||||||
|
a 400 is raised.
|
||||||
|
|
||||||
|
For this scenario a DataSchema checks that the relevant document has
|
||||||
|
a key in its data section, a key which is removed during the rendering
|
||||||
|
process as the document uses a delete action. This triggers
|
||||||
|
post-rendering validation failure.
|
||||||
|
"""
|
||||||
|
rules = {'deckhand:list_cleartext_documents': '@',
|
||||||
|
'deckhand:list_encrypted_documents': '@',
|
||||||
|
'deckhand:create_cleartext_documents': '@'}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
|
# Create a document for a bucket.
|
||||||
|
documents_factory = factories.DocumentFactory(2, [1, 1])
|
||||||
|
payload = documents_factory.gen_test({
|
||||||
|
"_GLOBAL_DATA_1_": {"data": {"a": "b"}},
|
||||||
|
"_SITE_DATA_1_": {"data": {"a": "b"}},
|
||||||
|
"_SITE_ACTIONS_1_": {
|
||||||
|
"actions": [{"method": "delete", "path": "."}]
|
||||||
|
}
|
||||||
|
}, site_abstract=False)
|
||||||
|
|
||||||
|
data_schema_factory = factories.DataSchemaFactory()
|
||||||
|
metadata_name = payload[-1]['schema']
|
||||||
|
schema_to_use = {
|
||||||
|
'$schema': 'http://json-schema.org/schema#',
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'a': {
|
||||||
|
'type': 'string'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'required': ['a'],
|
||||||
|
'additionalProperties': False
|
||||||
|
}
|
||||||
|
data_schema = data_schema_factory.gen_test(
|
||||||
|
metadata_name, data=schema_to_use)
|
||||||
|
payload.append(data_schema)
|
||||||
|
|
||||||
|
resp = self.app.simulate_put(
|
||||||
|
'/api/v1.0/buckets/mop/documents',
|
||||||
|
headers={'Content-Type': 'application/x-yaml'},
|
||||||
|
body=yaml.safe_dump_all(payload))
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
revision_id = list(yaml.safe_load_all(resp.text))[0]['status'][
|
||||||
|
'revision']
|
||||||
|
|
||||||
|
resp = self.app.simulate_get(
|
||||||
|
'/api/v1.0/revisions/%s/rendered-documents' % revision_id,
|
||||||
|
headers={'Content-Type': 'application/x-yaml'})
|
||||||
|
|
||||||
|
self.assertEqual(400, resp.status_code)
|
||||||
|
|
||||||
|
|
||||||
class TestRenderedDocumentsControllerNegativeRBAC(
|
class TestRenderedDocumentsControllerNegativeRBAC(
|
||||||
test_base.BaseControllerTest):
|
test_base.BaseControllerTest):
|
||||||
|
@ -277,13 +277,15 @@ class TestDocumentLayeringValidationNegative(
|
|||||||
|
|
||||||
layering_policy = copy.deepcopy(lp_template)
|
layering_policy = copy.deepcopy(lp_template)
|
||||||
del layering_policy['data']['layerOrder']
|
del layering_policy['data']['layerOrder']
|
||||||
error_re = ("The provided document\(s\) schema=%s, layer=%s, name=%s "
|
error_re = r"^'layerOrder' is a required property$"
|
||||||
"failed schema validation. Errors: 'layerOrder' is a "
|
e = self.assertRaises(
|
||||||
"required property" % (
|
errors.InvalidDocumentFormat, self._test_layering,
|
||||||
layering_policy['schema'],
|
|
||||||
layering_policy['metadata']['layeringDefinition'][
|
|
||||||
'layer'],
|
|
||||||
layering_policy['metadata']['name']))
|
|
||||||
self.assertRaisesRegexp(
|
|
||||||
errors.InvalidDocumentFormat, error_re, self._test_layering,
|
|
||||||
[layering_policy, document], validate=True)
|
[layering_policy, document], validate=True)
|
||||||
|
self.assertRegex(e.error_list[0]['message'], error_re)
|
||||||
|
self.assertEqual(layering_policy['schema'],
|
||||||
|
e.error_list[0]['documents'][0]['schema'])
|
||||||
|
self.assertEqual(layering_policy['metadata']['name'],
|
||||||
|
e.error_list[0]['documents'][0]['name'])
|
||||||
|
self.assertEqual(layering_policy['metadata']['layeringDefinition'][
|
||||||
|
'layer'],
|
||||||
|
e.error_list[0]['documents'][0]['layer'])
|
||||||
|
@ -149,21 +149,22 @@ class TestDocumentValidationNegative(test_base.TestDocumentValidationBase):
|
|||||||
parts = property_to_remove.split('.')
|
parts = property_to_remove.split('.')
|
||||||
missing_property = parts[-1]
|
missing_property = parts[-1]
|
||||||
|
|
||||||
expected_err = "'%s' is a required property" % missing_property
|
error_re = r"%s is a required property" % missing_property
|
||||||
self.assertIn(expected_err, e.message)
|
self.assertRegex(str(e.error_list).replace("\'", ""), error_re)
|
||||||
|
|
||||||
def test_document_invalid_layering_definition_action(self):
|
def test_document_invalid_layering_definition_action(self):
|
||||||
document = self._read_data('sample_document')
|
document = self._read_data('sample_document')
|
||||||
missing_data = self._corrupt_data(
|
missing_data = self._corrupt_data(
|
||||||
document, 'metadata.layeringDefinition.actions.0.method',
|
document, 'metadata.layeringDefinition.actions.0.method',
|
||||||
'invalid', op='replace')
|
'invalid', op='replace')
|
||||||
expected_err = (
|
error_re = (
|
||||||
r".+ 'invalid' is not one of \['replace', 'delete', 'merge'\]")
|
r".*invalid is not one of \[replace, delete, merge\]")
|
||||||
|
|
||||||
payload = [missing_data]
|
payload = [missing_data]
|
||||||
doc_validator = document_validation.DocumentValidation(payload)
|
doc_validator = document_validation.DocumentValidation(payload)
|
||||||
self.assertRaisesRegexp(errors.InvalidDocumentFormat, expected_err,
|
e = self.assertRaises(errors.InvalidDocumentFormat,
|
||||||
doc_validator.validate_all)
|
doc_validator.validate_all)
|
||||||
|
self.assertRegex(str(e.error_list[0]).replace("\'", ""), error_re)
|
||||||
|
|
||||||
def test_layering_policy_missing_required_sections(self):
|
def test_layering_policy_missing_required_sections(self):
|
||||||
properties_to_remove = (
|
properties_to_remove = (
|
||||||
|
Loading…
Reference in New Issue
Block a user