Add endpoint for GET /revisions.
This commit is contained in:
parent
3bc589e7fc
commit
0608801376
|
@ -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())
|
||||
]
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
|
@ -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'],
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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"])
|
Loading…
Reference in New Issue