From d02e1bcf5301312290c3f45d117e6aae7b9b521a Mon Sep 17 00:00:00 2001 From: Felipe Monteiro Date: Sun, 15 Apr 2018 13:36:19 -0400 Subject: [PATCH] [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] https://github.com/att-comdev/shipyard/blob/06b5e82ea881342572eb1c34a8d038b40fc20056/shipyard_airflow/control/configdocs/deckhand_client.py#L265 Change-Id: I827e5f47bffb23fa16ee5c8a705058034633baed --- deckhand/control/validations.py | 19 ++- deckhand/control/views/validation.py | 13 ++ deckhand/db/sqlalchemy/api.py | 25 ++- deckhand/service.py | 2 + .../control/test_validations_controller.py | 161 +++++++++++------- docs/source/api_ref.rst | 29 ++++ ...ons-details-endpoint-c3f18963b1372e40.yaml | 8 + 7 files changed, 184 insertions(+), 73 deletions(-) create mode 100644 releasenotes/notes/list-validations-details-endpoint-c3f18963b1372e40.yaml diff --git a/deckhand/control/validations.py b/deckhand/control/validations.py index 02ef5ec8..80bfb5f3 100644 --- a/deckhand/control/validations.py +++ b/deckhand/control/validations.py @@ -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) diff --git a/deckhand/control/views/validation.py b/deckhand/control/views/validation.py index e038750f..41d96287 100644 --- a/deckhand/control/views/validation.py +++ b/deckhand/control/views/validation.py @@ -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 = [] diff --git a/deckhand/db/sqlalchemy/api.py b/deckhand/db/sqlalchemy/api.py index f3b0783b..87086cc0 100644 --- a/deckhand/db/sqlalchemy/api.py +++ b/deckhand/db/sqlalchemy/api.py @@ -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() diff --git a/deckhand/service.py b/deckhand/service.py index 5c8dca38..10df3152 100644 --- a/deckhand/service.py +++ b/deckhand/service.py @@ -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}' diff --git a/deckhand/tests/unit/control/test_validations_controller.py b/deckhand/tests/unit/control/test_validations_controller.py index 8c757a79..2832b3df 100644 --- a/deckhand/tests/unit/control/test_validations_controller.py +++ b/deckhand/tests/unit/control/test_validations_controller.py @@ -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) diff --git a/docs/source/api_ref.rst b/docs/source/api_ref.rst index 3c2109ce..d8654281 100644 --- a/docs/source/api_ref.rst +++ b/docs/source/api_ref.rst @@ -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}}`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/releasenotes/notes/list-validations-details-endpoint-c3f18963b1372e40.yaml b/releasenotes/notes/list-validations-details-endpoint-c3f18963b1372e40.yaml new file mode 100644 index 00000000..ebba6162 --- /dev/null +++ b/releasenotes/notes/list-validations-details-endpoint-c3f18963b1372e40.yaml @@ -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.