Add Revision resource.

This commit is contained in:
Felipe Monteiro 2017-07-29 22:32:08 +01:00
parent adca9575b6
commit b44392bcb4
5 changed files with 101 additions and 19 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 revisions
from deckhand.control import secrets
from deckhand.db.sqlalchemy import api as db_api
@ -68,6 +69,7 @@ def start_api(state_manager=None):
v1_0_routes = [
('documents', documents.DocumentsResource()),
('revisions/{revision_id}/documents', revisions.RevisionsResource()),
('secrets', secrets.SecretsResource())
]

View File

@ -0,0 +1,45 @@
# 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.engine import document_validation
from deckhand import errors as deckhand_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 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.
"""
revision = db_api.revision_get(revision_id)
resp.status = falcon.HTTP_201
resp.body = revision['documents']

View File

@ -123,8 +123,9 @@ def document_create(values, session=None):
filters = models.Document.UNIQUE_CONSTRAINTS
try:
existing_document = document_get(**{c: values[c] for c in filters
if c != 'revision_id'})
existing_document = document_get(
raw_dict=True,
**{c: values[c] for c in filters if c != 'revision_id'})
except db_exception.DBError:
# Ignore bad data at this point. Allow creation to bubble up the error
# related to bad data.
@ -162,10 +163,10 @@ def document_create(values, session=None):
return created_document
def document_get(session=None, **filters):
def document_get(session=None, raw_dict=False, **filters):
session = session or get_session()
document = session.query(models.Document).filter_by(**filters).first()
return document.to_dict() if document else {}
return document.to_dict(raw_dict=raw_dict) if document else {}
####################

View File

@ -134,6 +134,13 @@ class Revision(BASE, DeckhandBase):
parent_id = Column(Integer, ForeignKey('revisions.id'), nullable=True)
child_id = Column(Integer, ForeignKey('revisions.id'), nullable=True)
documents = relationship("Document")
def to_dict(self):
d = super(Revision, self).to_dict()
d['documents'] = [doc.to_dict() for doc in self.documents]
return d
class Document(BASE, DeckhandBase):
UNIQUE_CONSTRAINTS = ('schema', 'name', 'revision_id')
@ -151,10 +158,18 @@ class Document(BASE, DeckhandBase):
data = Column(JSONEncodedDict(), nullable=False)
revision_id = Column(Integer, ForeignKey('revisions.id'), nullable=False)
revision = relationship("Revision",
foreign_keys=[revision_id],
cascade="all, delete")
def to_dict(self, raw_dict=False):
"""Convert the ``Document`` object into a dictionary format.
:param raw_dict: if True, returns unmodified data; else returns data
expected by users.
:returns: dictionary format of ``Document`` object.
"""
d = super(Document, self).to_dict()
# ``_metadata`` is used in the DB schema as ``metadata`` is reserved.
if not raw_dict:
d['metadata'] = d.pop('_metadata')
return d
def register_models(engine):
"""Create database tables for all models with the given engine."""

View File

@ -21,13 +21,15 @@ from testtools import matchers
from deckhand.db.sqlalchemy import api as db_api
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):
EXPECTED_FIELDS = ("created_at", "updated_at", "deleted_at", "deleted",
"id", "schema", "name", "_metadata", "data",
"revision_id")
@staticmethod
def get_minimal_fixture(**kwargs):
fixture = {'data': 'fake document data',
@ -54,9 +56,19 @@ class TestDocumentsApi(base.DeckhandWithDBTestCase):
self._validate_revision(revision)
return revision
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(DocumentFixture.EXPECTED_FIELDS)
expected_fields = list(DOCUMENT_EXPECTED_FIELDS)
if not is_deleted:
expected_fields.remove('deleted_at')
@ -64,9 +76,6 @@ class TestDocumentsApi(base.DeckhandWithDBTestCase):
for field in expected_fields:
self.assertIn(field, actual)
# ``_metadata`` is used in the DB schema as ``metadata`` is reserved.
actual['metadata'] = actual.pop('_metadata')
if expected:
# Validate that the expected values are equivalent to actual
# values.
@ -74,11 +83,10 @@ class TestDocumentsApi(base.DeckhandWithDBTestCase):
self.assertEqual(val, actual[key])
def _validate_revision(self, revision):
expected_attrs = ('id', 'child_id', 'parent_id')
for attr in expected_attrs:
self._validate_object(revision)
for attr in REVISION_EXPECTED_FIELDS:
self.assertIn(attr, revision)
self.assertThat(revision[attr], matchers.MatchesAny(
matchers.Is(None), matchers.IsInstance(unicode)))
def _validate_revision_connections(self, parent_document, parent_revision,
child_document, child_revision,
@ -200,3 +208,14 @@ class TestDocumentsApi(base.DeckhandWithDBTestCase):
self._validate_revision_connections(
parent_document, parent_revision, child_document, child_revision,
False)
def test_get_documents_by_revision_id(self):
payload = DocumentFixture.get_minimal_fixture()
document = self._create_document(payload)
revision = self._get_revision(document['revision_id'])
self.assertEqual(1, len(revision['documents']))
self.assertEqual(document, revision['documents'][0])
def test_get_multiple_documents_by_revision_id(self):
# TODO