Add endpoint for GET /revisions.

This commit is contained in:
Felipe Monteiro 2017-07-30 23:28:25 +01:00
parent 3bc589e7fc
commit 0608801376
9 changed files with 234 additions and 139 deletions

View File

@ -21,6 +21,7 @@ from oslo_log import log as logging
from deckhand.conf import config
from deckhand.control import base as api_base
from deckhand.control import documents
from deckhand.control import revision_documents
from deckhand.control import revisions
from deckhand.control import secrets
from deckhand.db.sqlalchemy import api as db_api
@ -69,7 +70,9 @@ def start_api(state_manager=None):
v1_0_routes = [
('documents', documents.DocumentsResource()),
('revisions/{revision_id}/documents', revisions.RevisionsResource()),
('revisions', revisions.RevisionsResource()),
('revisions/{revision_id}/documents',
revision_documents.RevisionDocumentsResource()),
('secrets', secrets.SecretsResource())
]

View File

@ -0,0 +1,50 @@
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import yaml
import falcon
from oslo_db import exception as db_exc
from oslo_log import log as logging
from oslo_serialization import jsonutils as json
from deckhand.control import base as api_base
from deckhand.db.sqlalchemy import api as db_api
from deckhand import errors
LOG = logging.getLogger(__name__)
class RevisionDocumentsResource(api_base.BaseResource):
"""API resource for realizing CRUD endpoints for Document Revisions."""
def on_get(self, req, resp, revision_id):
"""Returns all documents for a `revision_id`.
Returns a multi-document YAML response containing all the documents
matching the filters specified via query string parameters. Returned
documents will be as originally posted with no substitutions or
layering applied.
"""
params = req.params
try:
documents = db_api.revision_get_documents(revision_id, **params)
except errors.RevisionNotFound as e:
return self.return_error(resp, falcon.HTTP_403, message=e)
resp.status = falcon.HTTP_200
# TODO: return YAML-encoded body
resp.body = json.dumps(documents)

View File

@ -12,39 +12,23 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import yaml
import falcon
from oslo_db import exception as db_exc
from oslo_log import log as logging
from oslo_serialization import jsonutils as json
from deckhand.control import base as api_base
from deckhand.db.sqlalchemy import api as db_api
from deckhand import errors
LOG = logging.getLogger(__name__)
class RevisionsResource(api_base.BaseResource):
"""API resource for realizing CRUD endpoints for Document Revisions."""
def on_get(self, req, resp, revision_id):
"""Returns all documents for a `revision_id`.
"""Returns list of existing revisions.
Returns a multi-document YAML response containing all the documents
matching the filters specified via query string parameters. Returned
documents will be as originally posted with no substitutions or
layering applied.
Lists existing revisions and reports basic details including a summary
of validation status for each `deckhand/ValidationPolicy` that is part
of each revision.
"""
params = req.params
LOG.debug('PARAMS: %s' % params)
try:
documents = db_api.revision_get_documents(revision_id, **params)
except errors.RevisionNotFound as e:
return self.return_error(resp, falcon.HTTP_403, message=e)
revisions = db_api.revision_get_all()
resp.status = falcon.HTTP_200
resp.body = json.dumps(documents)
resp.body = json.dumps(revisions)

View File

@ -207,6 +207,20 @@ def revision_get(revision_id, session=None):
return revision
def revision_get_all(session=None):
"""Return list of all revisions."""
session = session or get_session()
revisions = session.query(models.Revision).all()
revisions_resp = []
for revision in revisions:
revision_dict = revision.to_dict()
revision['count'] = len(revision_dict.pop('documents'))
revisions_resp.append(revision)
return revisions_resp
def revision_get_documents(revision_id, session=None, **filters):
"""Return the documents that match filters for the specified `revision_id`.

View File

@ -19,6 +19,7 @@ import testtools
from deckhand.control import api
from deckhand.control import base as api_base
from deckhand.control import documents
from deckhand.control import revision_documents
from deckhand.control import revisions
from deckhand.control import secrets
@ -27,10 +28,11 @@ class TestApi(testtools.TestCase):
def setUp(self):
super(TestApi, self).setUp()
for resource in (documents, revisions, secrets):
for resource in (documents, revisions, revision_documents, secrets):
resource_name = resource.__name__.split('.')[-1]
resource_obj = mock.patch.object(
resource, '%sResource' % resource_name.title()).start()
resource, '%sResource' % resource_name.title().replace('_', '')
).start()
setattr(self, '%s_resource' % resource_name, resource_obj)
@mock.patch.object(api, 'db_api', autospec=True)
@ -47,8 +49,9 @@ class TestApi(testtools.TestCase):
request_type=api_base.DeckhandRequest)
mock_falcon_api.add_route.assert_has_calls([
mock.call('/api/v1.0/documents', self.documents_resource()),
mock.call('/api/v1.0/revisions', self.revisions_resource()),
mock.call('/api/v1.0/revisions/{revision_id}/documents',
self.revisions_resource()),
self.revision_documents_resource()),
mock.call('/api/v1.0/secrets', self.secrets_resource())
])
mock_config.parse_args.assert_called_once_with()

View File

@ -0,0 +1,115 @@
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import testtools
from testtools import matchers
from deckhand.db.sqlalchemy import api as db_api
from deckhand.tests import test_utils
from deckhand.tests.unit import base
BASE_EXPECTED_FIELDS = ("created_at", "updated_at", "deleted_at", "deleted")
DOCUMENT_EXPECTED_FIELDS = BASE_EXPECTED_FIELDS + (
"id", "schema", "name", "metadata", "data", "revision_id")
REVISION_EXPECTED_FIELDS = BASE_EXPECTED_FIELDS + (
"id", "child_id", "parent_id", "documents")
class DocumentFixture(object):
@staticmethod
def get_minimal_fixture(**kwargs):
fixture = {
'data': {
test_utils.rand_name('key'): test_utils.rand_name('value')
},
'metadata': {
'name': test_utils.rand_name('metadata_data'),
'label': test_utils.rand_name('metadata_label'),
'layeringDefinition': {
'abstract': test_utils.rand_bool(),
'layer': test_utils.rand_name('layer')
}
},
'schema': test_utils.rand_name('schema')}
fixture.update(kwargs)
return fixture
@staticmethod
def get_minimal_multi_fixture(count=2, **kwargs):
return [DocumentFixture.get_minimal_fixture(**kwargs)
for _ in range(count)]
class TestDbBase(base.DeckhandWithDBTestCase):
def _create_documents(self, payload):
if not isinstance(payload, list):
payload = [payload]
docs = db_api.documents_create(payload)
for idx, doc in enumerate(docs):
self._validate_document(expected=payload[idx], actual=doc)
return docs
def _get_document(self, **fields):
doc = db_api.document_get(**fields)
self._validate_document(actual=doc)
return doc
def _get_revision(self, revision_id):
revision = db_api.revision_get(revision_id)
self._validate_revision(revision)
return revision
def _get_revision_documents(self, revision_id, **filters):
documents = db_api.revision_get_documents(revision_id, **filters)
for document in documents:
self._validate_document(document)
return documents
def _list_revisions(self):
return db_api.revision_get_all()
def _validate_object(self, obj):
for attr in BASE_EXPECTED_FIELDS:
if attr.endswith('_at'):
self.assertThat(obj[attr], matchers.MatchesAny(
matchers.Is(None), matchers.IsInstance(str)))
else:
self.assertIsInstance(obj[attr], bool)
def _validate_document(self, actual, expected=None, is_deleted=False):
self._validate_object(actual)
# Validate that the document has all expected fields and is a dict.
expected_fields = list(DOCUMENT_EXPECTED_FIELDS)
if not is_deleted:
expected_fields.remove('deleted_at')
self.assertIsInstance(actual, dict)
for field in expected_fields:
self.assertIn(field, actual)
if expected:
# Validate that the expected values are equivalent to actual
# values.
for key, val in expected.items():
self.assertEqual(val, actual[key])
def _validate_revision(self, revision):
self._validate_object(revision)
for attr in REVISION_EXPECTED_FIELDS:
self.assertIn(attr, revision)

View File

@ -12,110 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import testtools
from testtools import matchers
from deckhand.db.sqlalchemy import api as db_api
from deckhand.tests import test_utils
from deckhand.tests.unit import base
BASE_EXPECTED_FIELDS = ("created_at", "updated_at", "deleted_at", "deleted")
DOCUMENT_EXPECTED_FIELDS = BASE_EXPECTED_FIELDS + (
"id", "schema", "name", "metadata", "data", "revision_id")
REVISION_EXPECTED_FIELDS = BASE_EXPECTED_FIELDS + (
"id", "child_id", "parent_id", "documents")
from deckhand.tests.unit.db import base
class DocumentFixture(object):
@staticmethod
def get_minimal_fixture(**kwargs):
fixture = {
'data': {
test_utils.rand_name('key'): test_utils.rand_name('value')
},
'metadata': {
'name': test_utils.rand_name('metadata_data'),
'label': test_utils.rand_name('metadata_label'),
'layeringDefinition': {
'abstract': test_utils.rand_bool(),
'layer': test_utils.rand_name('layer')
}
},
'schema': test_utils.rand_name('schema')}
fixture.update(kwargs)
return fixture
@staticmethod
def get_minimal_multi_fixture(count=2, **kwargs):
return [DocumentFixture.get_minimal_fixture(**kwargs)
for _ in range(count)]
class TestDocumentsBase(base.DeckhandWithDBTestCase):
def _create_documents(self, payload):
if not isinstance(payload, list):
payload = [payload]
docs = db_api.documents_create(payload)
for idx, doc in enumerate(docs):
self._validate_document(expected=payload[idx], actual=doc)
return docs
def _get_document(self, **fields):
doc = db_api.document_get(**fields)
self._validate_document(actual=doc)
return doc
def _get_revision(self, revision_id):
revision = db_api.revision_get(revision_id)
self._validate_revision(revision)
return revision
def _get_revision_documents(self, revision_id, **filters):
documents = db_api.revision_get_documents(revision_id, **filters)
for document in documents:
self._validate_document(document)
return documents
def _validate_object(self, obj):
for attr in BASE_EXPECTED_FIELDS:
if attr.endswith('_at'):
self.assertThat(obj[attr], matchers.MatchesAny(
matchers.Is(None), matchers.IsInstance(str)))
else:
self.assertIsInstance(obj[attr], bool)
def _validate_document(self, actual, expected=None, is_deleted=False):
self._validate_object(actual)
# Validate that the document has all expected fields and is a dict.
expected_fields = list(DOCUMENT_EXPECTED_FIELDS)
if not is_deleted:
expected_fields.remove('deleted_at')
self.assertIsInstance(actual, dict)
for field in expected_fields:
self.assertIn(field, actual)
if expected:
# Validate that the expected values are equivalent to actual
# values.
for key, val in expected.items():
self.assertEqual(val, actual[key])
def _validate_revision(self, revision):
self._validate_object(revision)
for attr in REVISION_EXPECTED_FIELDS:
self.assertIn(attr, revision)
class TestDocuments(TestDocumentsBase):
class TestDocuments(base.TestDbBase):
def test_create_and_get_document(self):
payload = DocumentFixture.get_minimal_fixture()
payload = base.DocumentFixture.get_minimal_fixture()
documents = self._create_documents(payload)
self.assertIsInstance(documents, list)
@ -126,7 +29,7 @@ class TestDocuments(TestDocumentsBase):
self.assertEqual(document, retrieved_document)
def test_create_document_again_with_no_changes(self):
payload = DocumentFixture.get_minimal_fixture()
payload = base.DocumentFixture.get_minimal_fixture()
self._create_documents(payload)
documents = self._create_documents(payload)
@ -134,7 +37,7 @@ class TestDocuments(TestDocumentsBase):
self.assertEmpty(documents)
def test_create_document_and_get_revision(self):
payload = DocumentFixture.get_minimal_fixture()
payload = base.DocumentFixture.get_minimal_fixture()
documents = self._create_documents(payload)
self.assertIsInstance(documents, list)
@ -146,7 +49,7 @@ class TestDocuments(TestDocumentsBase):
self.assertEqual(document['revision_id'], revision['id'])
def test_get_documents_by_revision_id(self):
payload = DocumentFixture.get_minimal_fixture()
payload = base.DocumentFixture.get_minimal_fixture()
documents = self._create_documents(payload)
revision = self._get_revision(documents[0]['revision_id'])
@ -154,7 +57,7 @@ class TestDocuments(TestDocumentsBase):
self.assertEqual(documents[0], revision['documents'][0])
def test_get_multiple_documents_by_revision_id(self):
payload = DocumentFixture.get_minimal_multi_fixture(count=3)
payload = base.DocumentFixture.get_minimal_multi_fixture(count=3)
documents = self._create_documents(payload)
self.assertIsInstance(documents, list)
@ -166,7 +69,7 @@ class TestDocuments(TestDocumentsBase):
self.assertEqual(document['revision_id'], revision['id'])
def test_get_documents_by_revision_id_and_filters(self):
payload = DocumentFixture.get_minimal_fixture()
payload = base.DocumentFixture.get_minimal_fixture()
document = self._create_documents(payload)[0]
filters = {
'schema': document['schema'],

View File

@ -12,18 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import testtools
from deckhand.db.sqlalchemy import api as db_api
from deckhand.tests import test_utils
from deckhand.tests.unit import base
from deckhand.tests.unit.db import test_documents
from deckhand.tests.unit.db import base
class TestDocumentsNegative(test_documents.TestDocumentsBase):
class TestDocumentsNegative(base.TestDbBase):
def test_get_documents_by_revision_id_and_wrong_filters(self):
payload = test_documents.DocumentFixture.get_minimal_fixture()
payload = base.DocumentFixture.get_minimal_fixture()
document = self._create_documents(payload)[0]
filters = {
'schema': 'fake_schema',

View File

@ -0,0 +1,28 @@
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from deckhand.tests.unit.db import base
class TestRevisions(base.TestDbBase):
def test_list_revisions(self):
payload = [base.DocumentFixture.get_minimal_fixture()
for _ in range(4)]
self._create_documents(payload)
revisions = self._list_revisions()
self.assertIsInstance(revisions, list)
self.assertEqual(1, len(revisions))
self.assertEqual(4, revisions[0]["count"])