From 841906a4353cd0fc88d23107b28be075dc98005e Mon Sep 17 00:00:00 2001 From: Felipe Monteiro Date: Mon, 31 Jul 2017 17:01:40 +0100 Subject: [PATCH] Updated /GET revisions response body. --- deckhand/control/README.rst | 26 ++++++++++++------- deckhand/control/base.py | 12 +++++++-- deckhand/control/documents.py | 5 ++-- deckhand/control/revisions.py | 2 +- deckhand/db/sqlalchemy/api.py | 14 ++++++++-- deckhand/db/sqlalchemy/models.py | 2 +- deckhand/engine/document_validation.py | 3 +-- deckhand/tests/unit/db/test_revisions.py | 6 ++++- .../unit/engine/test_document_validation.py | 19 ++++++++------ deckhand/utils.py | 7 +++++ 10 files changed, 68 insertions(+), 28 deletions(-) diff --git a/deckhand/control/README.rst b/deckhand/control/README.rst index 8b7a575e..243c4c70 100644 --- a/deckhand/control/README.rst +++ b/deckhand/control/README.rst @@ -79,15 +79,23 @@ Sample response: ```yaml --- -child_id: null -count: 2 -created_at: '2017-07-31T14:36:00.348967' -deleted: false -deleted_at: null -id: d3428d6a-d8c4-4a5b-8006-aba974cc36a2 -parent_id: null -results: [] -updated_at: '2017-07-31T14:36:00.348973' +count: 7 +next: https://deckhand/api/v1.0/revisions?limit=2&offset=2 +prev: null +results: + - id: 0 + url: https://deckhand/api/v1.0/revisions/0 + createdAt: 2017-07-14T21:23Z + validationPolicies: + site-deploy-validation: + status: failed + - id: 1 + url: https://deckhand/api/v1.0/revisions/1 + createdAt: 2017-07-16T01:15Z + validationPolicies: + site-deploy-validation: + status: succeeded +... ``` GET `/revisions/{revision_id}/documents` diff --git a/deckhand/control/base.py b/deckhand/control/base.py index 949f0759..d2975f20 100644 --- a/deckhand/control/base.py +++ b/deckhand/control/base.py @@ -17,11 +17,14 @@ import yaml import falcon from falcon import request +from oslo_log import log as logging from oslo_serialization import jsonutils as json import six from deckhand import errors +LOG = logging.getLogger(__name__) + class BaseResource(object): """Base resource class for implementing API resources.""" @@ -79,12 +82,17 @@ class BaseResource(object): resp.status = status_code def to_yaml_body(self, dict_body): - """Converts dictionary into YAML response body. + """Converts JSON body into YAML response body. :dict_body: response body to be converted to YAML. :returns: YAML encoding of `dict_body`. """ - return yaml.safe_dump_all(dict_body) + if isinstance(dict_body, dict): + return yaml.safe_dump(dict_body) + elif isinstance(dict_body, list): + return yaml.safe_dump_all(dict_body) + return TypeError('Unrecognized dict_body type when converting response' + ' body to YAML format.') class DeckhandRequestContext(object): diff --git a/deckhand/control/documents.py b/deckhand/control/documents.py index 3e9e90f0..b1d12910 100644 --- a/deckhand/control/documents.py +++ b/deckhand/control/documents.py @@ -47,10 +47,11 @@ class DocumentsResource(api_base.BaseResource): LOG.error(error_msg) return self.return_error(resp, falcon.HTTP_400, message=error_msg) - # Validate the document before doing anything with it. + # All concrete documents in the payload must successfully pass their + # JSON schema validations. Otherwise raise an error. try: for doc in documents: - document_validation.DocumentValidation(doc) + document_validation.DocumentValidation(doc).pre_validate() except deckhand_errors.InvalidFormat as e: return self.return_error(resp, falcon.HTTP_400, message=e) diff --git a/deckhand/control/revisions.py b/deckhand/control/revisions.py index e8032944..4956c193 100644 --- a/deckhand/control/revisions.py +++ b/deckhand/control/revisions.py @@ -19,7 +19,7 @@ from deckhand.db.sqlalchemy import api as db_api class RevisionsResource(api_base.BaseResource): - """API resource for realizing CRUD endpoints for Document Revisions.""" + """API resource for realizing CRUD endpoints for Revisions.""" def on_get(self, req, resp): """Returns list of existing revisions. diff --git a/deckhand/db/sqlalchemy/api.py b/deckhand/db/sqlalchemy/api.py index 2820fab1..3b3ce523 100644 --- a/deckhand/db/sqlalchemy/api.py +++ b/deckhand/db/sqlalchemy/api.py @@ -212,11 +212,21 @@ def revision_get_all(session=None): session = session or get_session() revisions = session.query(models.Revision).all() revisions_resp = [r.to_dict() for r in revisions] + resp_body = { + 'count': len(revisions_resp), + 'next': None, + 'prev': None, + 'revisions': [] + } for revision in revisions_resp: - revision['count'] = len(revision.pop('documents')) + result = {} + for attr in ('id', 'created_at'): + result[utils.to_camel_case(attr)] = revision[attr] + result['count'] = len(revision.pop('documents')) + resp_body['revisions'].append(result) - return revisions_resp + return resp_body def revision_get_documents(revision_id, session=None, **filters): diff --git a/deckhand/db/sqlalchemy/models.py b/deckhand/db/sqlalchemy/models.py index ca4bcf5b..348fe6ba 100644 --- a/deckhand/db/sqlalchemy/models.py +++ b/deckhand/db/sqlalchemy/models.py @@ -121,7 +121,7 @@ class Document(BASE, DeckhandBase): name = Column(String(64), nullable=False) # NOTE: Do not define a maximum length for these JSON data below. However, # this approach is not compatible with all database types. - # "metadata" is reserved, so use "doc_metadata" instead. + # "metadata" is reserved, so use "_metadata" instead. _metadata = Column(oslo_types.JsonEncodedDict(), nullable=False) data = Column(oslo_types.JsonEncodedDict(), nullable=False) revision_id = Column(Integer, ForeignKey('revisions.id'), nullable=False) diff --git a/deckhand/engine/document_validation.py b/deckhand/engine/document_validation.py index 44494fb1..7dc5ae0c 100644 --- a/deckhand/engine/document_validation.py +++ b/deckhand/engine/document_validation.py @@ -34,7 +34,6 @@ class DocumentValidation(object): def __init__(self, data): self.data = data - self.pre_validate_data() class SchemaVersion(object): """Class for retrieving correct schema for pre-validation on YAML. @@ -65,7 +64,7 @@ class DocumentValidation(object): return [v['schema'] for v in self.internal_validations if v['version'] == self.schema_version][0].schema - def pre_validate_data(self): + def pre_validate(self): """Pre-validate that the YAML file is correctly formatted.""" self._validate_with_schema() diff --git a/deckhand/tests/unit/db/test_revisions.py b/deckhand/tests/unit/db/test_revisions.py index 9bb8f6e0..1bfd4fe2 100644 --- a/deckhand/tests/unit/db/test_revisions.py +++ b/deckhand/tests/unit/db/test_revisions.py @@ -23,6 +23,10 @@ class TestRevisions(base.TestDbBase): self._create_documents(payload) revisions = self._list_revisions() - self.assertIsInstance(revisions, list) + self.assertIsInstance(revisions, dict) + self.assertIn('revisions', revisions) + self.assertIsInstance(revisions['revisions'], list) + + revisions = revisions['revisions'] self.assertEqual(1, len(revisions)) self.assertEqual(4, revisions[0]["count"]) diff --git a/deckhand/tests/unit/engine/test_document_validation.py b/deckhand/tests/unit/engine/test_document_validation.py index eeaee958..2b2ff1a6 100644 --- a/deckhand/tests/unit/engine/test_document_validation.py +++ b/deckhand/tests/unit/engine/test_document_validation.py @@ -71,10 +71,8 @@ class TestDocumentValidation(testtools.TestCase): return corrupted_data def test_initialization(self): - doc_validation = document_validation.DocumentValidation( - self.data) - self.assertIsInstance(doc_validation, - document_validation.DocumentValidation) + doc_validation = document_validation.DocumentValidation(self.data) + doc_validation.pre_validate() # Should not raise any errors. def test_initialization_missing_sections(self): expected_err = ("The provided YAML file is invalid. Exception: '%s' " @@ -91,11 +89,12 @@ class TestDocumentValidation(testtools.TestCase): for invalid_entry, missing_key in invalid_data: with six.assertRaisesRegex(self, errors.InvalidFormat, expected_err % missing_key): - document_validation.DocumentValidation(invalid_entry) + doc_validation = document_validation.DocumentValidation( + invalid_entry) + doc_validation.pre_validate() def test_initialization_missing_abstract_section(self): expected_err = ("Could not find 'abstract' property from document.") - invalid_data = [ self._corrupt_data('metadata'), self._corrupt_data('metadata.layeringDefinition'), @@ -105,7 +104,9 @@ class TestDocumentValidation(testtools.TestCase): for invalid_entry in invalid_data: with six.assertRaisesRegex(self, errors.InvalidFormat, expected_err): - document_validation.DocumentValidation(invalid_entry) + doc_validation = document_validation.DocumentValidation( + invalid_entry) + doc_validation.pre_validate() @mock.patch.object(document_validation, 'LOG', autospec=True) def test_initialization_with_abstract_document(self, mock_log): @@ -114,7 +115,9 @@ class TestDocumentValidation(testtools.TestCase): for true_val in (True, 'true', 'True'): abstract_data['metadata']['layeringDefinition']['abstract'] = True - document_validation.DocumentValidation(abstract_data) + doc_validation = document_validation.DocumentValidation( + abstract_data) + doc_validation.pre_validate() mock_log.info.assert_called_once_with( "Skipping validation for the document because it is abstract") mock_log.info.reset_mock() diff --git a/deckhand/utils.py b/deckhand/utils.py index 0e44eefd..18fba501 100644 --- a/deckhand/utils.py +++ b/deckhand/utils.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import string + def multi_getattr(multi_key, dict_data): """Iteratively check for nested attributes in the YAML data. @@ -45,3 +47,8 @@ def multi_getattr(multi_key, dict_data): data = data.get(attr) return data + + +def to_camel_case(s): + return (s[0].lower() + string.capwords(s, sep='_').replace('_', '')[1:] + if s else s)