[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:
Felipe Monteiro 2018-04-15 13:36:19 -04:00 committed by Scott Hussey
parent 236e8be530
commit d02e1bcf53
7 changed files with 184 additions and 73 deletions

View File

@ -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)

View File

@ -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 = []

View File

@ -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()

View File

@ -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}'

View File

@ -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)

View File

@ -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}}``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -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.