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.