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
from shipyard_airflow.control.configdocs.deckhand_client import (
DeckhandClient, DeckhandPaths, DeckhandRejectedInputError,
DeckhandClient, DeckhandError, DeckhandPaths, DeckhandRejectedInputError,
DeckhandResponseError, DocumentExistsElsewhereError, NoRevisionsExistError)
from shipyard_airflow.control.service_endpoints import (
Endpoints, get_endpoint, get_token)
@ -342,8 +342,15 @@ class ConfigdocsHelper(object):
if version in (BUFFER, COMMITTED):
if revision_dict.get(version):
revision_id = revision_dict.get(version).get('id')
return self.deckhand.get_rendered_docs_from_revision(
revision_id=revision_id)
try:
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:
raise ApiError(
title='This revision does not exist',
@ -469,14 +476,10 @@ class ConfigdocsHelper(object):
}
exception['exception'] = ex
def get_validations_for_revision(self, revision_id):
"""
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.
"""
def _get_validations_from_ucp_components(self, revision_id):
"""Invoke other UCP components to retrieve their validations"""
resp_msgs = []
error_count = 0
validation_threads = ConfigdocsHelper._get_validation_threads(
ConfigdocsHelper._get_validation_endpoints(), revision_id,
@ -490,7 +493,6 @@ class ConfigdocsHelper(object):
validation_thread.get('thread').join()
# check on the response, extract the validations
error_count = 0
for validation_thread in validation_threads:
th_name = validation_thread.get('name')
val_response = validation_thread.get('response',
@ -517,12 +519,43 @@ class ConfigdocsHelper(object):
source=th_name
)
resp_msgs.append(val_msg)
# Deckhand does it differently. Incorporate those validation
# failures, this may have to change if we store validations from other
# sources in Deckhand.
dh_validations = self._get_deckhand_validations(revision_id)
return (error_count, resp_msgs)
def get_validations_for_revision(self, 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)
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 ConfigdocsHelper._format_validations_to_status(
resp_msgs, error_count)
@ -537,9 +570,8 @@ class ConfigdocsHelper(object):
return ConfigdocsHelper._format_validations_to_status(
dh_validations, error_count)
def _get_deckhand_validations(self, revision_id):
# Returns any validations that deckhand has on hand for this
# revision.
def _get_deckhand_validation_errors(self, revision_id):
# Returns stored validation errors that deckhand has for this revision.
resp_msgs = []
deckhand_val = self.deckhand.get_all_revision_validations(revision_id)
if deckhand_val.get('results'):

View File

@ -207,6 +207,31 @@ class DeckhandClient(object):
self._handle_bad_response(response)
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):
"""
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
#
class DeckhandStatefulError(Exception):
"""
Base exception for errors that indicate some stateful-based
condition in deckhand. Not intended for use directly
"""
def __init__(self, response_message=None):
super().__init__()
self.response_message = response_message
class DeckhandStatefulError(DeckhandError):
"""Base exception for errors for stateful-based conflicts in Deckhand."""
pass
class NoRevisionsExistError(DeckhandStatefulError):
@ -495,15 +526,14 @@ class DocumentExistsElsewhereError(DeckhandStatefulError):
#
class DeckhandResponseError(Exception):
class DeckhandResponseError(DeckhandError):
"""
Indicates that a response was returned from
Deckhand that was not expected
"""
def __init__(self, status_code, response_message=None):
super().__init__()
super().__init__(response_message)
self.status_code = status_code
self.response_message = response_message
class DeckhandRejectedInputError(DeckhandResponseError):
@ -518,12 +548,10 @@ class DeckhandRejectedInputError(DeckhandResponseError):
#
class DeckhandAccessError(Exception):
class DeckhandAccessError(DeckhandError):
"""
Used to indicate that accessing Deckhand has failed.
This is not the same as a bad response from Deckhand:
See DeckhandResponseError
"""
def __init__(self, response_message=None):
super().__init__()
self.response_message = response_message
pass

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import mock
from unittest.mock import patch
import yaml
@ -23,7 +24,7 @@ from shipyard_airflow.control.configdocs import configdocs_helper
from shipyard_airflow.control.configdocs.configdocs_helper import (
BufferMode, ConfigdocsHelper)
from shipyard_airflow.control.configdocs.deckhand_client import (
DeckhandClient, DeckhandPaths, DeckhandResponseError,
DeckhandClient, DeckhandResponseError,
NoRevisionsExistError)
from shipyard_airflow.errors import ApiError, AppError
@ -504,18 +505,19 @@ def test_get_collection_docs():
assert len(yaml_str) == 16
def _fake_get_validation_endpoints():
val_ep = '{}/validatedesign'
return [
{
'name': 'Drydock',
'url': val_ep.format('drydock')
},
{
'name': 'Armada',
'url': val_ep.format('armada')
},
]
val_ep = '{}/validatedesign'
val_endpoints = [
{
'name': 'Drydock',
'url': val_ep.format('drydock')
},
{
'name': 'Armada',
'url': val_ep.format('armada')
},
]
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,
"messageList": [
{ "message" : "broke it 1", "error": true},
{ "message" : "speeling error", "error": true}
{ "message" : "speeling error", "error": true},
{ "message" : "good things", "error": false}
]
},
"code": 400
}
""") % 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
"""
with patch('shipyard_airflow.control.configdocs.deckhand_client.'
'DeckhandClient.get_path') as mock_get_path:
mock_get_path.return_value = 'path{}'
helper = ConfigdocsHelper(CTX)
hold_ve = helper.__class__._get_validation_endpoints
hold_vfc = helper.__class__._get_validations_for_component
helper.__class__._get_validation_endpoints = (
_fake_get_validation_endpoints)
helper.__class__._get_validations_for_component = (
_fake_get_validations_for_component)
helper._get_deckhand_validations = lambda revision_id: []
try:
val_status = helper.get_validations_for_revision(3)
err_count = val_status['details']['errorCount']
err_list_count = len(val_status['details']['messageList'])
assert err_count == err_list_count
assert val_status['details']['errorCount'] == 4
finally:
helper.__class__._get_validation_endpoints = hold_ve
helper.__class__._get_validations_for_component = hold_vfc
mock_get_path.assert_called_with(DeckhandPaths.RENDERED_REVISION_DOCS)
helper = ConfigdocsHelper(CTX)
hold_ve = helper.__class__._get_validation_endpoints
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_count == err_list_count
assert val_status['details']['errorCount'] == 1
assert val_status['details']['messageList'][0]['message'] == 'broken!'
@mock.patch.object(DeckhandClient, 'get_render_errors',
return_value=[])
@mock.patch.object(DeckhandClient, 'get_path',
return_value='path{}')
@mock.patch.object(ConfigdocsHelper, '_get_validation_endpoints',
return_value=val_endpoints)
@mock.patch.object(ConfigdocsHelper, '_get_validations_for_component',
new=_fake_get_validations_for_component)
def test_get_validations_for_revision(p1, p2, p3):
"""
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():
@ -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
"""
@ -757,7 +773,7 @@ def test__get_deckhand_validations():
lambda reivsion_id, subset_name: FK_VAL_SUBSET_RESP)
helper.deckhand._get_entry_validation_response = (
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(
@ -786,7 +802,7 @@ def test__get_deckhand_validations_empty_errors():
lambda reivsion_id, subset_name: FK_VAL_SUBSET_RESP)
helper.deckhand._get_entry_validation_response = (
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(
@ -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
"""
helper = ConfigdocsHelper(CTX)
helper.deckhand._get_base_validation_resp = (
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():