[feature] Endpoint for listing revision validations with details
This patch set adds a new endpoint to the Validations API which allows
for listing all validations for a given revision with details.
The response body for GET /api/v1.0/{revision_id}/validations/detail
looks like:
---
count: 1
next: null
prev: null
results:
- name: promenade-site-validation
url: https://deckhand/api/v1.0/revisions/4/validations/promenade-site-validation/entries/0
status: failure
createdAt: 2017-07-16T02:03Z
expiresAfter: null
expiresAt: null
errors:
- documents:
- schema: promenade/Node/v1
name: node-document-name
- schema: promenade/Masters/v1
name: kubernetes-masters
message: Node has master role, but not included in cluster masters list.
Note that the Validations API in general is currently missing fields
like url (as well as next and prev references) which will be included
in a follow up.
This will enable Shipyard to avoid performing a quadratic number
of API look ups when querying Deckhand's Validations API: [0].
The policy enforced for this endpoint is deckhand:list_validations.
APIImpact
DocImpact
[0] 06b5e82ea8/shipyard_airflow/control/configdocs/deckhand_client.py (L265)
Change-Id: I827e5f47bffb23fa16ee5c8a705058034633baed
This commit is contained in:
parent
236e8be530
commit
d02e1bcf53
@ -49,7 +49,6 @@ class ValidationsResource(api_base.BaseResource):
|
||||
LOG.exception(e.format_message())
|
||||
|
||||
resp.status = falcon.HTTP_201
|
||||
resp.append_header('Content-Type', 'application/x-yaml')
|
||||
resp.body = self.view_builder.show(resp_body)
|
||||
|
||||
def on_get(self, req, resp, revision_id, validation_name=None,
|
||||
@ -64,7 +63,6 @@ class ValidationsResource(api_base.BaseResource):
|
||||
resp_body = self._list_all_validations(req, resp, revision_id)
|
||||
|
||||
resp.status = falcon.HTTP_200
|
||||
resp.append_header('Content-Type', 'application/x-yaml')
|
||||
resp.body = resp_body
|
||||
|
||||
@policy.authorize('deckhand:show_validation')
|
||||
@ -110,3 +108,20 @@ class ValidationsResource(api_base.BaseResource):
|
||||
|
||||
resp_body = self.view_builder.list(validations)
|
||||
return resp_body
|
||||
|
||||
|
||||
class ValidationsDetailsResource(api_base.BaseResource):
|
||||
"""API resource for listing revision validations with details."""
|
||||
|
||||
view_builder = validation_view.ViewBuilder()
|
||||
|
||||
@policy.authorize('deckhand:list_validations')
|
||||
def on_get(self, req, resp, revision_id):
|
||||
try:
|
||||
entries = db_api.validation_get_all_entries(revision_id,
|
||||
val_name=None)
|
||||
except errors.RevisionNotFound as e:
|
||||
raise falcon.HTTPNotFound(description=e.format_message())
|
||||
|
||||
resp.status = falcon.HTTP_200
|
||||
resp.body = self.view_builder.detail(entries)
|
||||
|
@ -28,6 +28,19 @@ class ViewBuilder(common.ViewBuilder):
|
||||
]
|
||||
}
|
||||
|
||||
def detail(self, entries):
|
||||
results = []
|
||||
|
||||
for idx, entry in enumerate(entries):
|
||||
formatted_entry = self.show_entry(entry)
|
||||
formatted_entry.setdefault('id', idx)
|
||||
results.append(formatted_entry)
|
||||
|
||||
return {
|
||||
'count': len(results),
|
||||
'results': results
|
||||
}
|
||||
|
||||
def list_entries(self, entries):
|
||||
results = []
|
||||
|
||||
|
@ -1116,14 +1116,10 @@ def validation_get_all(revision_id, session=None):
|
||||
return result.values()
|
||||
|
||||
|
||||
@require_revision_exists
|
||||
def validation_get_all_entries(revision_id, val_name, session=None):
|
||||
def _check_validation_entries_against_validation_policies(
|
||||
revision_id, entries, val_name=None, session=None):
|
||||
session = session or get_session()
|
||||
|
||||
entries = session.query(models.Validation)\
|
||||
.filter_by(**{'revision_id': revision_id, 'name': val_name})\
|
||||
.order_by(models.Validation.created_at.asc())\
|
||||
.all()
|
||||
result = [e.to_dict() for e in entries]
|
||||
result_map = {}
|
||||
for r in result:
|
||||
@ -1148,7 +1144,7 @@ def validation_get_all_entries(revision_id, val_name, session=None):
|
||||
# If an entry in the ValidationPolicy was never POSTed, set its status
|
||||
# to failure.
|
||||
for missing_name in missing_validations:
|
||||
if missing_name == val_name:
|
||||
if val_name is None or missing_name == val_name:
|
||||
result.append({
|
||||
'id': len(result),
|
||||
'name': val_name,
|
||||
@ -1186,6 +1182,21 @@ def validation_get_all_entries(revision_id, val_name, session=None):
|
||||
return result
|
||||
|
||||
|
||||
@require_revision_exists
|
||||
def validation_get_all_entries(revision_id, val_name=None, session=None):
|
||||
session = session or get_session()
|
||||
|
||||
entries = session.query(models.Validation)\
|
||||
.filter_by(revision_id=revision_id)
|
||||
if val_name:
|
||||
entries = entries.filter_by(name=val_name)
|
||||
entries.order_by(models.Validation.created_at.asc())\
|
||||
.all()
|
||||
|
||||
return _check_validation_entries_against_validation_policies(
|
||||
revision_id, entries, val_name=val_name, session=session)
|
||||
|
||||
|
||||
@require_revision_exists
|
||||
def validation_get_entry(revision_id, val_name, entry_id, session=None):
|
||||
session = session or get_session()
|
||||
|
@ -54,6 +54,8 @@ def configure_app(app, version=''):
|
||||
revision_tags.RevisionTagsResource()),
|
||||
('revisions/{revision_id}/validations',
|
||||
validations.ValidationsResource()),
|
||||
('revisions/{revision_id}/validations/detail',
|
||||
validations.ValidationsDetailsResource()),
|
||||
('revisions/{revision_id}/validations/{validation_name}',
|
||||
validations.ValidationsResource()),
|
||||
('revisions/{revision_id}/validations/{validation_name}'
|
||||
|
@ -53,7 +53,7 @@ validator:
|
||||
"""
|
||||
|
||||
|
||||
class ValidationsControllerBaseTest(test_base.BaseControllerTest):
|
||||
class BaseValidationsControllerTest(test_base.BaseControllerTest):
|
||||
|
||||
def _create_revision(self, payload=None):
|
||||
if not payload:
|
||||
@ -97,14 +97,8 @@ class ValidationsControllerBaseTest(test_base.BaseControllerTest):
|
||||
self.addCleanup(mock.patch.stopall)
|
||||
|
||||
|
||||
class TestValidationsControllerPostValidate(ValidationsControllerBaseTest):
|
||||
"""Test suite for validating positive scenarios for post-validations with
|
||||
Validations controller.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestValidationsControllerPostValidate, self).setUp()
|
||||
self._monkey_patch_document_validation()
|
||||
class TestValidationsController(BaseValidationsControllerTest):
|
||||
"""Test suite for validating Validations API."""
|
||||
|
||||
def test_create_validation(self):
|
||||
rules = {'deckhand:create_cleartext_documents': '@',
|
||||
@ -194,7 +188,7 @@ class TestValidationsControllerPostValidate(ValidationsControllerBaseTest):
|
||||
revision_id = self._create_revision()
|
||||
|
||||
# Validate that 3 entries (1 for each of the 3 documents created)
|
||||
# exists for
|
||||
# exists for:
|
||||
# /api/v1.0/revisions/1/validations/deckhand-schema-validation
|
||||
resp = self.app.simulate_get(
|
||||
'/api/v1.0/revisions/%s/validations/%s' % (
|
||||
@ -364,6 +358,98 @@ class TestValidationsControllerPostValidate(ValidationsControllerBaseTest):
|
||||
self.assertEqual(404, resp.status_code)
|
||||
self.assertEqual(expected_error, yaml.safe_load(resp.text)['message'])
|
||||
|
||||
def test_list_validations_details(self):
|
||||
rules = {'deckhand:create_cleartext_documents': '@',
|
||||
'deckhand:list_validations': '@'}
|
||||
self.policy.set_rules(rules)
|
||||
|
||||
revision_id = self._create_revision()
|
||||
|
||||
# Validate that 3 entries (1 for each of the 3 documents created)
|
||||
# exists for
|
||||
# /api/v1.0/revisions/1/validations/deckhand-schema-validation
|
||||
resp = self.app.simulate_get(
|
||||
'/api/v1.0/revisions/%s/validations/detail' % revision_id,
|
||||
headers={'Content-Type': 'application/x-yaml'})
|
||||
self.assertEqual(200, resp.status_code)
|
||||
body = yaml.safe_load(resp.text)
|
||||
expected_body = {
|
||||
'results': [{
|
||||
'createdAt': None,
|
||||
'errors': [],
|
||||
'expiresAfter': None,
|
||||
'id': idx,
|
||||
'name': 'deckhand-schema-validation',
|
||||
'status': 'success'
|
||||
|
||||
} for idx in range(3)],
|
||||
'count': 3
|
||||
}
|
||||
self.assertEqual(expected_body, body)
|
||||
|
||||
|
||||
class TestValidationsControllerPreValidate(BaseValidationsControllerTest):
|
||||
"""Test suite for validating positive scenarios for pre-validations with
|
||||
Validations controller.
|
||||
"""
|
||||
|
||||
def test_pre_validate_flag_skips_registered_dataschema_validations(self):
|
||||
rules = {'deckhand:create_cleartext_documents': '@',
|
||||
'deckhand:list_validations': '@'}
|
||||
self.policy.set_rules(rules)
|
||||
|
||||
# Create a `DataSchema` against which the test document will be
|
||||
# validated.
|
||||
data_schema_factory = factories.DataSchemaFactory()
|
||||
metadata_name = 'example/foo/v1'
|
||||
schema_to_use = {
|
||||
'$schema': 'http://json-schema.org/schema#',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'a': {
|
||||
'type': 'integer' # Test doc will fail b/c of wrong type.
|
||||
}
|
||||
},
|
||||
'required': ['a']
|
||||
}
|
||||
data_schema = data_schema_factory.gen_test(
|
||||
metadata_name, data=schema_to_use)
|
||||
|
||||
# Create a document that passes validation and another that fails it.
|
||||
doc_factory = factories.DocumentFactory(1, [1])
|
||||
fail_doc = doc_factory.gen_test(
|
||||
{'_GLOBAL_DATA_1_': {'data': {'a': 'fail'}}},
|
||||
global_abstract=False)[-1]
|
||||
fail_doc['schema'] = 'example/foo/v1'
|
||||
fail_doc['metadata']['name'] = 'test_doc'
|
||||
|
||||
revision_id = self._create_revision(payload=[data_schema, fail_doc])
|
||||
|
||||
# Validate that the validation reports success because `fail_doc`
|
||||
# isn't validated by the `DataSchema`.
|
||||
resp = self.app.simulate_get(
|
||||
'/api/v1.0/revisions/%s/validations' % revision_id,
|
||||
headers={'Content-Type': 'application/x-yaml'})
|
||||
self.assertEqual(200, resp.status_code)
|
||||
body = yaml.safe_load(resp.text)
|
||||
expected_body = {
|
||||
'count': 1,
|
||||
'results': [
|
||||
{'name': types.DECKHAND_SCHEMA_VALIDATION, 'status': 'success'}
|
||||
]
|
||||
}
|
||||
self.assertEqual(expected_body, body)
|
||||
|
||||
|
||||
class TestValidationsControllerPostValidate(BaseValidationsControllerTest):
|
||||
"""Test suite for validating positive scenarios for post-validations with
|
||||
Validations controller.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestValidationsControllerPostValidate, self).setUp()
|
||||
self._monkey_patch_document_validation()
|
||||
|
||||
def test_validation_with_registered_data_schema(self):
|
||||
rules = {'deckhand:create_cleartext_documents': '@',
|
||||
'deckhand:list_validations': '@'}
|
||||
@ -817,7 +903,7 @@ class TestValidationsControllerPostValidate(ValidationsControllerBaseTest):
|
||||
|
||||
|
||||
class TestValidationsControllerWithValidationPolicy(
|
||||
ValidationsControllerBaseTest):
|
||||
BaseValidationsControllerTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestValidationsControllerWithValidationPolicy, self).setUp()
|
||||
@ -1157,56 +1243,3 @@ data:
|
||||
|
||||
_do_test(VALIDATION_SUCCESS_RESULT, 'success')
|
||||
_do_test(VALIDATION_FAILURE_RESULT, 'failure')
|
||||
|
||||
|
||||
class TestValidationsControllerPreValidate(ValidationsControllerBaseTest):
|
||||
"""Test suite for validating positive scenarios for pre-validations with
|
||||
Validations controller.
|
||||
"""
|
||||
|
||||
def test_pre_validate_flag_skips_registered_dataschema_validations(self):
|
||||
rules = {'deckhand:create_cleartext_documents': '@',
|
||||
'deckhand:list_validations': '@'}
|
||||
self.policy.set_rules(rules)
|
||||
|
||||
# Create a `DataSchema` against which the test document will be
|
||||
# validated.
|
||||
data_schema_factory = factories.DataSchemaFactory()
|
||||
metadata_name = 'example/foo/v1'
|
||||
schema_to_use = {
|
||||
'$schema': 'http://json-schema.org/schema#',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'a': {
|
||||
'type': 'integer' # Test doc will fail b/c of wrong type.
|
||||
}
|
||||
},
|
||||
'required': ['a']
|
||||
}
|
||||
data_schema = data_schema_factory.gen_test(
|
||||
metadata_name, data=schema_to_use)
|
||||
|
||||
# Create a document that passes validation and another that fails it.
|
||||
doc_factory = factories.DocumentFactory(1, [1])
|
||||
fail_doc = doc_factory.gen_test(
|
||||
{'_GLOBAL_DATA_1_': {'data': {'a': 'fail'}}},
|
||||
global_abstract=False)[-1]
|
||||
fail_doc['schema'] = 'example/foo/v1'
|
||||
fail_doc['metadata']['name'] = 'test_doc'
|
||||
|
||||
revision_id = self._create_revision(payload=[data_schema, fail_doc])
|
||||
|
||||
# Validate that the validation reports success because `fail_doc`
|
||||
# isn't validated by the `DataSchema`.
|
||||
resp = self.app.simulate_get(
|
||||
'/api/v1.0/revisions/%s/validations' % revision_id,
|
||||
headers={'Content-Type': 'application/x-yaml'})
|
||||
self.assertEqual(200, resp.status_code)
|
||||
body = yaml.safe_load(resp.text)
|
||||
expected_body = {
|
||||
'count': 1,
|
||||
'results': [
|
||||
{'name': types.DECKHAND_SCHEMA_VALIDATION, 'status': 'success'}
|
||||
]
|
||||
}
|
||||
self.assertEqual(expected_body, body)
|
||||
|
@ -336,6 +336,35 @@ Sample response:
|
||||
url: https://deckhand/api/v1.0/revisions/4/validations/promenade-site-validation
|
||||
status: failure
|
||||
|
||||
GET ``/revisions/{{revision_id}}/validations/detail``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Gets the list of validations, with details, which have been reported for this
|
||||
revision.
|
||||
|
||||
Sample response:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
---
|
||||
count: 1
|
||||
next: null
|
||||
prev: null
|
||||
results:
|
||||
- name: promenade-site-validation
|
||||
url: https://deckhand/api/v1.0/revisions/4/validations/promenade-site-validation/entries/0
|
||||
status: failure
|
||||
createdAt: 2017-07-16T02:03Z
|
||||
expiresAfter: null
|
||||
expiresAt: null
|
||||
errors:
|
||||
- documents:
|
||||
- schema: promenade/Node/v1
|
||||
name: node-document-name
|
||||
- schema: promenade/Masters/v1
|
||||
name: kubernetes-masters
|
||||
message: Node has master role, but not included in cluster masters list.
|
||||
|
||||
GET ``/revisions/{{revision_id}}/validations/{{name}}``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Adds a new endpoint to the Deckhand Validations API,
|
||||
GET /api/v1.0/{revision_id}/validations/detail, which allows for the
|
||||
possibility of listing all validations for a revision with details.
|
||||
The response body includes all details returned by retrieving
|
||||
validation details for a specific validation entry.
|
Loading…
Reference in New Issue
Block a user