Shipyard use deckhand render validations

For a commit configdocs, updates the code to first call the
Deckhand rendered-documents endpoint to get validations before
deciding whether or not to proceed to the other components.

Change-Id: I26bb7cf15f582f887edf4cd01a39378da3b0aa0a
This commit is contained in:
Bryan Strassner 2018-02-08 17:56:25 -06:00
parent c3c712107f
commit 06b5e82ea8
3 changed files with 151 additions and 75 deletions

View File

@ -26,7 +26,7 @@ from oslo_config import cfg
import requests import requests
from shipyard_airflow.control.configdocs.deckhand_client import ( from shipyard_airflow.control.configdocs.deckhand_client import (
DeckhandClient, DeckhandPaths, DeckhandRejectedInputError, DeckhandClient, DeckhandError, DeckhandPaths, DeckhandRejectedInputError,
DeckhandResponseError, DocumentExistsElsewhereError, NoRevisionsExistError) DeckhandResponseError, DocumentExistsElsewhereError, NoRevisionsExistError)
from shipyard_airflow.control.service_endpoints import ( from shipyard_airflow.control.service_endpoints import (
Endpoints, get_endpoint, get_token) Endpoints, get_endpoint, get_token)
@ -342,8 +342,15 @@ class ConfigdocsHelper(object):
if version in (BUFFER, COMMITTED): if version in (BUFFER, COMMITTED):
if revision_dict.get(version): if revision_dict.get(version):
revision_id = revision_dict.get(version).get('id') revision_id = revision_dict.get(version).get('id')
return self.deckhand.get_rendered_docs_from_revision( try:
revision_id=revision_id) return self.deckhand.get_rendered_docs_from_revision(
revision_id=revision_id)
except DeckhandError as de:
raise ApiError(
title='Deckhand indicated an error while rendering',
description=de.response_message,
status=falcon.HTTP_500,
retry=False)
else: else:
raise ApiError( raise ApiError(
title='This revision does not exist', title='This revision does not exist',
@ -469,14 +476,10 @@ class ConfigdocsHelper(object):
} }
exception['exception'] = ex exception['exception'] = ex
def get_validations_for_revision(self, revision_id): def _get_validations_from_ucp_components(self, revision_id):
""" """Invoke other UCP components to retrieve their validations"""
Use the endpoints for each of the UCP components to validate the
version indicated. Uses:
https://github.com/att-comdev/ucp-integration/blob/master/docs/api-conventions.md#post-v10validatedesign
format.
"""
resp_msgs = [] resp_msgs = []
error_count = 0
validation_threads = ConfigdocsHelper._get_validation_threads( validation_threads = ConfigdocsHelper._get_validation_threads(
ConfigdocsHelper._get_validation_endpoints(), revision_id, ConfigdocsHelper._get_validation_endpoints(), revision_id,
@ -490,7 +493,6 @@ class ConfigdocsHelper(object):
validation_thread.get('thread').join() validation_thread.get('thread').join()
# check on the response, extract the validations # check on the response, extract the validations
error_count = 0
for validation_thread in validation_threads: for validation_thread in validation_threads:
th_name = validation_thread.get('name') th_name = validation_thread.get('name')
val_response = validation_thread.get('response', val_response = validation_thread.get('response',
@ -517,12 +519,43 @@ class ConfigdocsHelper(object):
source=th_name source=th_name
) )
resp_msgs.append(val_msg) resp_msgs.append(val_msg)
# Deckhand does it differently. Incorporate those validation return (error_count, resp_msgs)
# failures, this may have to change if we store validations from other
# sources in Deckhand. def get_validations_for_revision(self, revision_id):
dh_validations = self._get_deckhand_validations(revision_id) """Retrieves validations for a revision
Invokes Deckhand to render the revision, which will either succeed, or
fail and return validaiton failures. If there are any failures, the
process will not proceed to validate against the other UCP components.
Upon success from Deckhand rendering, uses the endpoints for each of
the UCP components to validate the version indicated.
Responds in the format defined here:
https://github.com/att-comdev/ucp-integration/blob/master/docs/api-conventions.md#post-v10validatedesign
"""
resp_msgs = []
error_count = 0
# Capture the messages from trying to render the revision.
render_errors = self.deckhand.get_render_errors(revision_id)
resp_msgs.extend(render_errors)
error_count += len(render_errors)
LOG.debug("Deckhand errors from rendering: %s", error_count)
# Incorporate stored validation errors from Deckhand (prevalidations)
# Note: This may have to change to be later in the code if we store
# validations from other sources in Deckhand.
dh_validations = self._get_deckhand_validation_errors(revision_id)
error_count += len(dh_validations) error_count += len(dh_validations)
resp_msgs.extend(dh_validations) resp_msgs.extend(dh_validations)
LOG.debug("Deckhand validations: %s", len(dh_validations))
# Only invoke the other validations if Deckhand has not returned any.
if (error_count == 0):
(cpnt_ec, cpnt_msgs) = self._get_validations_from_ucp_components(
revision_id)
resp_msgs.extend(cpnt_msgs)
error_count += cpnt_ec
LOG.debug("UCP component validations: %s", cpnt_ec)
# return the formatted status response # return the formatted status response
return ConfigdocsHelper._format_validations_to_status( return ConfigdocsHelper._format_validations_to_status(
resp_msgs, error_count) resp_msgs, error_count)
@ -537,9 +570,8 @@ class ConfigdocsHelper(object):
return ConfigdocsHelper._format_validations_to_status( return ConfigdocsHelper._format_validations_to_status(
dh_validations, error_count) dh_validations, error_count)
def _get_deckhand_validations(self, revision_id): def _get_deckhand_validation_errors(self, revision_id):
# Returns any validations that deckhand has on hand for this # Returns stored validation errors that deckhand has for this revision.
# revision.
resp_msgs = [] resp_msgs = []
deckhand_val = self.deckhand.get_all_revision_validations(revision_id) deckhand_val = self.deckhand.get_all_revision_validations(revision_id)
if deckhand_val.get('results'): if deckhand_val.get('results'):

View File

@ -207,6 +207,31 @@ class DeckhandClient(object):
self._handle_bad_response(response) self._handle_bad_response(response)
return response.text return response.text
def get_render_errors(self, revision_id):
"""Retrieve any error messages from rendering configdocs or []
Entries in the list returned are {error: ..., message: ...} format.
"""
url = DeckhandClient.get_path(
DeckhandPaths.RENDERED_REVISION_DOCS
).format(revision_id)
errors = []
LOG.debug("Retrieving rendered docs checking for validation messages")
response = self._get_request(url)
if response.status_code >= 400:
err_resp = yaml.safe_load(response.text)
errors = err_resp.get('details', {}).get('messageList', [])
if not errors:
# default message if none were specified.
errors.append({
"error": True,
"message": ("Deckhand has reported an error but did not "
"specify messages. Response: {}".format(
response.text))})
return errors
def get_rendered_docs_from_revision(self, revision_id, bucket_id=None): def get_rendered_docs_from_revision(self, revision_id, bucket_id=None):
""" """
Returns the full set of rendered documents for a revision Returns the full set of rendered documents for a revision
@ -459,19 +484,25 @@ class DeckhandClient(object):
) )
) )
#
# Exceptions
#
class DeckhandError(Exception):
"""Base exception for for all exceptions raised by this client"""
def __init__(self, response_message=None):
super().__init__()
self.response_message = response_message
# #
# Deckhand stateful messages wrapped as exceptions # Deckhand stateful messages wrapped as exceptions
# #
class DeckhandStatefulError(Exception): class DeckhandStatefulError(DeckhandError):
""" """Base exception for errors for stateful-based conflicts in Deckhand."""
Base exception for errors that indicate some stateful-based pass
condition in deckhand. Not intended for use directly
"""
def __init__(self, response_message=None):
super().__init__()
self.response_message = response_message
class NoRevisionsExistError(DeckhandStatefulError): class NoRevisionsExistError(DeckhandStatefulError):
@ -495,15 +526,14 @@ class DocumentExistsElsewhereError(DeckhandStatefulError):
# #
class DeckhandResponseError(Exception): class DeckhandResponseError(DeckhandError):
""" """
Indicates that a response was returned from Indicates that a response was returned from
Deckhand that was not expected Deckhand that was not expected
""" """
def __init__(self, status_code, response_message=None): def __init__(self, status_code, response_message=None):
super().__init__() super().__init__(response_message)
self.status_code = status_code self.status_code = status_code
self.response_message = response_message
class DeckhandRejectedInputError(DeckhandResponseError): class DeckhandRejectedInputError(DeckhandResponseError):
@ -518,12 +548,10 @@ class DeckhandRejectedInputError(DeckhandResponseError):
# #
class DeckhandAccessError(Exception): class DeckhandAccessError(DeckhandError):
""" """
Used to indicate that accessing Deckhand has failed. Used to indicate that accessing Deckhand has failed.
This is not the same as a bad response from Deckhand: This is not the same as a bad response from Deckhand:
See DeckhandResponseError See DeckhandResponseError
""" """
def __init__(self, response_message=None): pass
super().__init__()
self.response_message = response_message

View File

@ -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 json import json
import mock
from unittest.mock import patch from unittest.mock import patch
import yaml import yaml
@ -23,7 +24,7 @@ from shipyard_airflow.control.configdocs import configdocs_helper
from shipyard_airflow.control.configdocs.configdocs_helper import ( from shipyard_airflow.control.configdocs.configdocs_helper import (
BufferMode, ConfigdocsHelper) BufferMode, ConfigdocsHelper)
from shipyard_airflow.control.configdocs.deckhand_client import ( from shipyard_airflow.control.configdocs.deckhand_client import (
DeckhandClient, DeckhandPaths, DeckhandResponseError, DeckhandClient, DeckhandResponseError,
NoRevisionsExistError) NoRevisionsExistError)
from shipyard_airflow.errors import ApiError, AppError from shipyard_airflow.errors import ApiError, AppError
@ -504,18 +505,19 @@ def test_get_collection_docs():
assert len(yaml_str) == 16 assert len(yaml_str) == 16
def _fake_get_validation_endpoints(): val_ep = '{}/validatedesign'
val_ep = '{}/validatedesign'
return [
{ val_endpoints = [
'name': 'Drydock', {
'url': val_ep.format('drydock') 'name': 'Drydock',
}, 'url': val_ep.format('drydock')
{ },
'name': 'Armada', {
'url': val_ep.format('armada') 'name': 'Armada',
}, 'url': val_ep.format('armada')
] },
]
def _fake_get_validations_for_component(url, design_reference, response, def _fake_get_validations_for_component(url, design_reference, response,
@ -535,40 +537,54 @@ def _fake_get_validations_for_component(url, design_reference, response,
"errorCount": 2, "errorCount": 2,
"messageList": [ "messageList": [
{ "message" : "broke it 1", "error": true}, { "message" : "broke it 1", "error": true},
{ "message" : "speeling error", "error": true} { "message" : "speeling error", "error": true},
{ "message" : "good things", "error": false}
] ]
}, },
"code": 400 "code": 400
} }
""") % url) """) % url)
def test_get_validations_for_revision(): dh_render_val_list = [{"error": True, "message": "broken!"}]
@mock.patch.object(DeckhandClient, 'get_render_errors',
return_value=dh_render_val_list)
def test_get_validations_for_revision_dh_render(get_endpoint):
""" """
Tests the functionality of the get_validations_for_revision method Tests the functionality of the get_validations_for_revision method
""" """
with patch('shipyard_airflow.control.configdocs.deckhand_client.' helper = ConfigdocsHelper(CTX)
'DeckhandClient.get_path') as mock_get_path: hold_ve = helper.__class__._get_validation_endpoints
mock_get_path.return_value = 'path{}' helper._get_deckhand_validation_errors = lambda revision_id: []
helper = ConfigdocsHelper(CTX) val_status = helper.get_validations_for_revision(3)
hold_ve = helper.__class__._get_validation_endpoints err_count = val_status['details']['errorCount']
hold_vfc = helper.__class__._get_validations_for_component err_list_count = len(val_status['details']['messageList'])
helper.__class__._get_validation_endpoints = ( assert err_count == err_list_count
_fake_get_validation_endpoints) assert val_status['details']['errorCount'] == 1
helper.__class__._get_validations_for_component = ( assert val_status['details']['messageList'][0]['message'] == 'broken!'
_fake_get_validations_for_component)
helper._get_deckhand_validations = lambda revision_id: []
try: @mock.patch.object(DeckhandClient, 'get_render_errors',
val_status = helper.get_validations_for_revision(3) return_value=[])
err_count = val_status['details']['errorCount'] @mock.patch.object(DeckhandClient, 'get_path',
err_list_count = len(val_status['details']['messageList']) return_value='path{}')
assert err_count == err_list_count @mock.patch.object(ConfigdocsHelper, '_get_validation_endpoints',
assert val_status['details']['errorCount'] == 4 return_value=val_endpoints)
finally: @mock.patch.object(ConfigdocsHelper, '_get_validations_for_component',
helper.__class__._get_validation_endpoints = hold_ve new=_fake_get_validations_for_component)
helper.__class__._get_validations_for_component = hold_vfc def test_get_validations_for_revision(p1, p2, p3):
mock_get_path.assert_called_with(DeckhandPaths.RENDERED_REVISION_DOCS) """
Tests the functionality of the get_validations_for_revision method
"""
helper = ConfigdocsHelper(CTX)
helper._get_deckhand_validation_errors = lambda revision_id: []
val_status = helper.get_validations_for_revision(3)
err_count = val_status['details']['errorCount']
err_list_count = len(val_status['details']['messageList'])
assert err_list_count == 6
assert val_status['details']['errorCount'] == 4
def test_generate_validation_message(): def test_generate_validation_message():
@ -746,7 +762,7 @@ errors:
""") """)
def test__get_deckhand_validations(): def test__get_deckhand_validation_errors():
""" """
Tets the functionality of processing a response from deckhand Tets the functionality of processing a response from deckhand
""" """
@ -757,7 +773,7 @@ def test__get_deckhand_validations():
lambda reivsion_id, subset_name: FK_VAL_SUBSET_RESP) lambda reivsion_id, subset_name: FK_VAL_SUBSET_RESP)
helper.deckhand._get_entry_validation_response = ( helper.deckhand._get_entry_validation_response = (
lambda reivsion_id, subset_name, entry_id: FK_VAL_ENTRY_RESP) lambda reivsion_id, subset_name, entry_id: FK_VAL_ENTRY_RESP)
assert len(helper._get_deckhand_validations(5)) == 2 assert len(helper._get_deckhand_validation_errors(5)) == 2
FK_VAL_ENTRY_RESP_EMPTY = FakeResponse( FK_VAL_ENTRY_RESP_EMPTY = FakeResponse(
@ -786,7 +802,7 @@ def test__get_deckhand_validations_empty_errors():
lambda reivsion_id, subset_name: FK_VAL_SUBSET_RESP) lambda reivsion_id, subset_name: FK_VAL_SUBSET_RESP)
helper.deckhand._get_entry_validation_response = ( helper.deckhand._get_entry_validation_response = (
lambda reivsion_id, subset_name, entry_id: FK_VAL_ENTRY_RESP_EMPTY) lambda reivsion_id, subset_name, entry_id: FK_VAL_ENTRY_RESP_EMPTY)
assert len(helper._get_deckhand_validations(5)) == 0 assert len(helper._get_deckhand_validation_errors(5)) == 0
FK_VAL_BASE_RESP_EMPTY = FakeResponse( FK_VAL_BASE_RESP_EMPTY = FakeResponse(
@ -801,14 +817,14 @@ results: []
""") """)
def test__get_deckhand_validations_empty_results(): def test__get_deckhand_validation_errors_empty_results():
""" """
Tets the functionality of processing a response from deckhand Tets the functionality of processing a response from deckhand
""" """
helper = ConfigdocsHelper(CTX) helper = ConfigdocsHelper(CTX)
helper.deckhand._get_base_validation_resp = ( helper.deckhand._get_base_validation_resp = (
lambda revision_id: FK_VAL_BASE_RESP_EMPTY) lambda revision_id: FK_VAL_BASE_RESP_EMPTY)
assert len(helper._get_deckhand_validations(5)) == 0 assert len(helper._get_deckhand_validation_errors(5)) == 0
def test_tag_buffer(): def test_tag_buffer():