Only allow one LayeringPolicy to exist in the system.
This PS enforces the design requirement that says that only 1 layering policy can exist in the system at once. Attempting to create another layering policy with a different name is a 409 error. The existing layering policy can be updated by passing in a document with the same `metadata.name` and `schema` as the existing one. Closes-Bug: https://github.com/att-comdev/deckhand/issues/12 Change-Id: I7cad2d600c931c8701c3faaf2967be782984528b
This commit is contained in:
parent
2ba761a36c
commit
55b13dc4eb
@ -90,7 +90,8 @@ class BucketsResource(api_base.BaseResource):
|
|||||||
try:
|
try:
|
||||||
created_documents = db_api.documents_create(
|
created_documents = db_api.documents_create(
|
||||||
bucket_name, documents, validations=validations)
|
bucket_name, documents, validations=validations)
|
||||||
except deckhand_errors.DocumentExists as e:
|
except (deckhand_errors.DocumentExists,
|
||||||
|
deckhand_errors.SingletonDocumentConflict) as e:
|
||||||
raise falcon.HTTPConflict(description=e.format_message())
|
raise falcon.HTTPConflict(description=e.format_message())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise falcon.HTTPInternalServerError(description=six.text_type(e))
|
raise falcon.HTTPInternalServerError(description=six.text_type(e))
|
||||||
|
@ -56,13 +56,17 @@ class ValidationsResource(api_base.BaseResource):
|
|||||||
def on_get(self, req, resp, revision_id, validation_name=None,
|
def on_get(self, req, resp, revision_id, validation_name=None,
|
||||||
entry_id=None):
|
entry_id=None):
|
||||||
if all([validation_name, entry_id]):
|
if all([validation_name, entry_id]):
|
||||||
self._show_validation_entry(
|
resp_body = self._show_validation_entry(
|
||||||
req, resp, revision_id, validation_name, entry_id)
|
req, resp, revision_id, validation_name, entry_id)
|
||||||
elif validation_name:
|
elif validation_name:
|
||||||
self._list_validation_entries(req, resp, revision_id,
|
resp_body = self._list_validation_entries(req, resp, revision_id,
|
||||||
validation_name)
|
validation_name)
|
||||||
else:
|
else:
|
||||||
self._list_all_validations(req, resp, revision_id)
|
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')
|
@policy.authorize('deckhand:show_validation')
|
||||||
def _show_validation_entry(self, req, resp, revision_id, validation_name,
|
def _show_validation_entry(self, req, resp, revision_id, validation_name,
|
||||||
@ -80,9 +84,7 @@ class ValidationsResource(api_base.BaseResource):
|
|||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
raise falcon.HTTPNotFound(description=e.format_message())
|
||||||
|
|
||||||
resp_body = self.view_builder.show_entry(entry)
|
resp_body = self.view_builder.show_entry(entry)
|
||||||
resp.status = falcon.HTTP_200
|
return resp_body
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
|
||||||
resp.body = resp_body
|
|
||||||
|
|
||||||
@policy.authorize('deckhand:list_validations')
|
@policy.authorize('deckhand:list_validations')
|
||||||
def _list_validation_entries(self, req, resp, revision_id,
|
def _list_validation_entries(self, req, resp, revision_id,
|
||||||
@ -94,9 +96,7 @@ class ValidationsResource(api_base.BaseResource):
|
|||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
raise falcon.HTTPNotFound(description=e.format_message())
|
||||||
|
|
||||||
resp_body = self.view_builder.list_entries(entries)
|
resp_body = self.view_builder.list_entries(entries)
|
||||||
resp.status = falcon.HTTP_200
|
return resp_body
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
|
||||||
resp.body = resp_body
|
|
||||||
|
|
||||||
@policy.authorize('deckhand:list_validations')
|
@policy.authorize('deckhand:list_validations')
|
||||||
def _list_all_validations(self, req, resp, revision_id):
|
def _list_all_validations(self, req, resp, revision_id):
|
||||||
@ -104,8 +104,6 @@ class ValidationsResource(api_base.BaseResource):
|
|||||||
validations = db_api.validation_get_all(revision_id)
|
validations = db_api.validation_get_all(revision_id)
|
||||||
except errors.RevisionNotFound as e:
|
except errors.RevisionNotFound as e:
|
||||||
raise falcon.HTTPNotFound(description=e.format_message())
|
raise falcon.HTTPNotFound(description=e.format_message())
|
||||||
resp_body = self.view_builder.list(validations)
|
|
||||||
|
|
||||||
resp.status = falcon.HTTP_200
|
resp_body = self.view_builder.list(validations)
|
||||||
resp.append_header('Content-Type', 'application/x-yaml')
|
return resp_body
|
||||||
resp.body = resp_body
|
|
||||||
|
@ -98,6 +98,53 @@ def raw_query(query, **kwargs):
|
|||||||
return get_engine().execute(stmt)
|
return get_engine().execute(stmt)
|
||||||
|
|
||||||
|
|
||||||
|
def require_unique_document_schema(schema=None):
|
||||||
|
"""Decorator to enforce only one singleton document exists in the system.
|
||||||
|
|
||||||
|
An example of a singleton document is a ``LayeringPolicy`` document.
|
||||||
|
|
||||||
|
Only one singleton document can exist within the system at any time. It is
|
||||||
|
an error to attempt to insert a new document with the same ``schema`` if it
|
||||||
|
has a different ``metadata.name`` than the existing document.
|
||||||
|
|
||||||
|
A singleton document that already exists can be updated, if the document
|
||||||
|
that is passed in has the same name/schema as the existing one.
|
||||||
|
|
||||||
|
The existing singleton document can be replaced by first deleting it
|
||||||
|
and only then creating a new one.
|
||||||
|
|
||||||
|
:raises SingletonDocumentConflict: if a singleton document in the system
|
||||||
|
already exists and any of the documents to be created has the same
|
||||||
|
``schema`` but has a ``metadata.name`` that differs from the one
|
||||||
|
already registered.
|
||||||
|
"""
|
||||||
|
def decorator(f):
|
||||||
|
if schema not in types.DOCUMENT_SCHEMA_TYPES:
|
||||||
|
raise errors.DeckhandException(
|
||||||
|
'Unrecognized document schema %s.' % schema)
|
||||||
|
|
||||||
|
@functools.wraps(f)
|
||||||
|
def wrapper(bucket_name, documents, *args, **kwargs):
|
||||||
|
existing_documents = revision_get_documents(
|
||||||
|
schema=schema, deleted=False, include_history=False)
|
||||||
|
existing_document_names = [x['name'] for x in existing_documents]
|
||||||
|
# `conflict_names` is calculated by checking whether any documents
|
||||||
|
# in `documents` is a layering policy with a name not found in
|
||||||
|
# `existing_documents`.
|
||||||
|
conflicting_names = [
|
||||||
|
x['metadata']['name'] for x in documents
|
||||||
|
if x['metadata']['name'] not in existing_document_names and
|
||||||
|
x['schema'].startswith(schema)]
|
||||||
|
if existing_document_names and conflicting_names:
|
||||||
|
raise errors.SingletonDocumentConflict(
|
||||||
|
document=existing_document_names[0],
|
||||||
|
conflict=conflicting_names)
|
||||||
|
return f(bucket_name, documents, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
@require_unique_document_schema(types.LAYERING_POLICY_SCHEMA)
|
||||||
def documents_create(bucket_name, documents, validations=None,
|
def documents_create(bucket_name, documents, validations=None,
|
||||||
session=None):
|
session=None):
|
||||||
"""Create a set of documents and associated bucket.
|
"""Create a set of documents and associated bucket.
|
||||||
@ -203,7 +250,8 @@ def _documents_create(bucket_name, values_list, session=None):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
existing_document = document_get(
|
existing_document = document_get(
|
||||||
raw_dict=True, **{x: values[x] for x in filters})
|
raw_dict=True, deleted=False,
|
||||||
|
**{x: values[x] for x in filters})
|
||||||
except errors.DocumentNotFound:
|
except errors.DocumentNotFound:
|
||||||
# Ignore bad data at this point. Allow creation to bubble up the
|
# Ignore bad data at this point. Allow creation to bubble up the
|
||||||
# error related to bad data.
|
# error related to bad data.
|
||||||
@ -214,7 +262,8 @@ def _documents_create(bucket_name, values_list, session=None):
|
|||||||
# Ignore redundant validation policies as they are allowed to exist
|
# Ignore redundant validation policies as they are allowed to exist
|
||||||
# in multiple buckets.
|
# in multiple buckets.
|
||||||
if (existing_document['bucket_name'] != bucket_name and
|
if (existing_document['bucket_name'] != bucket_name and
|
||||||
existing_document['schema'] != types.VALIDATION_POLICY_SCHEMA):
|
not existing_document['schema'].startswith(
|
||||||
|
types.VALIDATION_POLICY_SCHEMA)):
|
||||||
raise errors.DocumentExists(
|
raise errors.DocumentExists(
|
||||||
schema=existing_document['schema'],
|
schema=existing_document['schema'],
|
||||||
name=existing_document['name'],
|
name=existing_document['name'],
|
||||||
@ -644,7 +693,7 @@ def revision_get_documents(revision_id=None, include_history=True,
|
|||||||
.filter_by(id=revision_id)\
|
.filter_by(id=revision_id)\
|
||||||
.one()
|
.one()
|
||||||
else:
|
else:
|
||||||
# If no revision_id is specified, grab the newest one.
|
# If no revision_id is specified, grab the latest one.
|
||||||
revision = session.query(models.Revision)\
|
revision = session.query(models.Revision)\
|
||||||
.order_by(models.Revision.created_at.desc())\
|
.order_by(models.Revision.created_at.desc())\
|
||||||
.first()
|
.first()
|
||||||
|
@ -188,6 +188,14 @@ class DocumentExists(DeckhandException):
|
|||||||
code = 409
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class SingletonDocumentConflict(DeckhandException):
|
||||||
|
msg_fmt = ("A singleton document by the name %(document)s already "
|
||||||
|
"exists in the system. The new document %(conflict)s cannot be "
|
||||||
|
"created. To create a document with a new name, delete the "
|
||||||
|
"current one first.")
|
||||||
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
class LayeringPolicyNotFound(DeckhandException):
|
class LayeringPolicyNotFound(DeckhandException):
|
||||||
msg_fmt = ("LayeringPolicy with schema %(schema)s not found in the "
|
msg_fmt = ("LayeringPolicy with schema %(schema)s not found in the "
|
||||||
"system.")
|
"system.")
|
||||||
|
@ -93,7 +93,7 @@ class DocumentFactory(DeckhandFactory):
|
|||||||
"layerOrder": []
|
"layerOrder": []
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "layering-policy",
|
"name": "placeholder",
|
||||||
"schema": "metadata/Control/v%s" % DeckhandFactory.API_VERSION
|
"schema": "metadata/Control/v%s" % DeckhandFactory.API_VERSION
|
||||||
},
|
},
|
||||||
"schema": "deckhand/LayeringPolicy/v%s" % DeckhandFactory.API_VERSION
|
"schema": "deckhand/LayeringPolicy/v%s" % DeckhandFactory.API_VERSION
|
||||||
@ -157,7 +157,10 @@ class DocumentFactory(DeckhandFactory):
|
|||||||
layer_order = ["global", "region", "site"]
|
layer_order = ["global", "region", "site"]
|
||||||
else:
|
else:
|
||||||
raise ValueError("'num_layers' must be a value between 1 - 3.")
|
raise ValueError("'num_layers' must be a value between 1 - 3.")
|
||||||
self.LAYERING_DEFINITION['data']['layerOrder'] = layer_order
|
self.layering_definition = copy.deepcopy(self.LAYERING_DEFINITION)
|
||||||
|
self.layering_definition['metadata']['name'] = test_utils.rand_name(
|
||||||
|
'layering-policy')
|
||||||
|
self.layering_definition['data']['layerOrder'] = layer_order
|
||||||
|
|
||||||
if not isinstance(docs_per_layer, (list, tuple)):
|
if not isinstance(docs_per_layer, (list, tuple)):
|
||||||
raise TypeError("'docs_per_layer' must be a list or tuple "
|
raise TypeError("'docs_per_layer' must be a list or tuple "
|
||||||
@ -223,7 +226,7 @@ class DocumentFactory(DeckhandFactory):
|
|||||||
:type site_parent_selectors: list
|
:type site_parent_selectors: list
|
||||||
:returns: Rendered template of the form specified above.
|
:returns: Rendered template of the form specified above.
|
||||||
"""
|
"""
|
||||||
rendered_template = [self.LAYERING_DEFINITION]
|
rendered_template = [self.layering_definition]
|
||||||
layer_order = rendered_template[0]['data']['layerOrder']
|
layer_order = rendered_template[0]['data']['layerOrder']
|
||||||
|
|
||||||
for layer_idx in range(self.num_layers):
|
for layer_idx in range(self.num_layers):
|
||||||
|
@ -19,6 +19,7 @@ from oslo_config import cfg
|
|||||||
|
|
||||||
from deckhand.control import buckets
|
from deckhand.control import buckets
|
||||||
from deckhand import factories
|
from deckhand import factories
|
||||||
|
from deckhand.tests import test_utils
|
||||||
from deckhand.tests.unit.control import base as test_base
|
from deckhand.tests.unit.control import base as test_base
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -134,6 +135,32 @@ schema:
|
|||||||
self.assertEqual(400, resp.status_code)
|
self.assertEqual(400, resp.status_code)
|
||||||
self.assertRegexpMatches(resp.text, error_re[idx])
|
self.assertRegexpMatches(resp.text, error_re[idx])
|
||||||
|
|
||||||
|
def test_put_conflicting_layering_policy(self):
|
||||||
|
rules = {'deckhand:create_cleartext_documents': '@'}
|
||||||
|
self.policy.set_rules(rules)
|
||||||
|
|
||||||
|
payload = factories.DocumentFactory(1, [1]).gen_test({})[0]
|
||||||
|
|
||||||
|
# Create the first layering policy.
|
||||||
|
resp = self.app.simulate_put(
|
||||||
|
'/api/v1.0/bucket/mop/documents',
|
||||||
|
headers={'Content-Type': 'application/x-yaml'},
|
||||||
|
body=yaml.safe_dump_all([payload]))
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
|
||||||
|
# Validate that a layering policy with a different, conflicting name
|
||||||
|
# raises the expected exception.
|
||||||
|
error_re = ('.*A singleton document by the name %s already exists in '
|
||||||
|
'the system.' % payload['metadata']['name'])
|
||||||
|
payload['metadata']['name'] = test_utils.rand_name('layering-policy')
|
||||||
|
resp = self.app.simulate_put(
|
||||||
|
'/api/v1.0/bucket/mop/documents',
|
||||||
|
headers={'Content-Type': 'application/x-yaml'},
|
||||||
|
body=yaml.safe_dump_all([payload]))
|
||||||
|
self.assertEqual(409, resp.status_code)
|
||||||
|
resp_error = ' '.join(resp.text.split())
|
||||||
|
self.assertRegexpMatches(resp_error, error_re)
|
||||||
|
|
||||||
|
|
||||||
class TestBucketsControllerNegativeRBAC(test_base.BaseControllerTest):
|
class TestBucketsControllerNegativeRBAC(test_base.BaseControllerTest):
|
||||||
"""Test suite for validating negative RBAC scenarios for bucket
|
"""Test suite for validating negative RBAC scenarios for bucket
|
||||||
|
61
deckhand/tests/unit/db/test_layering_policies.py
Normal file
61
deckhand/tests/unit/db/test_layering_policies.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# 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 import errors
|
||||||
|
from deckhand import factories
|
||||||
|
from deckhand.tests import test_utils
|
||||||
|
from deckhand.tests.unit.db import base
|
||||||
|
|
||||||
|
|
||||||
|
class LayeringPoliciesBaseTest(base.TestDbBase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(LayeringPoliciesBaseTest, self).setUp()
|
||||||
|
# Will create 3 documents: layering policy, plus a global and site
|
||||||
|
# document.
|
||||||
|
self.documents_factory = factories.DocumentFactory(2, [1, 1])
|
||||||
|
self.document_mapping = {
|
||||||
|
"_GLOBAL_DATA_1_": {"data": {"a": {"x": 1, "y": 2}}},
|
||||||
|
"_SITE_DATA_1_": {"data": {"a": {"x": 7, "z": 3}, "b": 4}},
|
||||||
|
"_SITE_ACTIONS_1_": {
|
||||||
|
"actions": [{"method": "merge", "path": "."}]}
|
||||||
|
}
|
||||||
|
self.bucket_name = test_utils.rand_name('bucket')
|
||||||
|
|
||||||
|
def _create_layering_policy(self):
|
||||||
|
payload = self.documents_factory.gen_test(self.document_mapping)
|
||||||
|
self.create_documents(self.bucket_name, payload)
|
||||||
|
return payload[0]
|
||||||
|
|
||||||
|
|
||||||
|
class TestLayeringPolicies(LayeringPoliciesBaseTest):
|
||||||
|
|
||||||
|
def test_update_layering_policy(self):
|
||||||
|
layering_policy = self._create_layering_policy()
|
||||||
|
layering_policy['data'] = {'data': {'layerOrder': ['region', 'site']}}
|
||||||
|
self.create_documents(self.bucket_name, [layering_policy])
|
||||||
|
|
||||||
|
def test_create_new_layering_policy_after_first_deleted(self):
|
||||||
|
self._create_layering_policy()
|
||||||
|
self.create_documents(self.bucket_name, [])
|
||||||
|
self._create_layering_policy()
|
||||||
|
|
||||||
|
|
||||||
|
class TestLayeringPoliciesNegative(LayeringPoliciesBaseTest):
|
||||||
|
|
||||||
|
def test_create_conflicting_layering_policy_fails(self):
|
||||||
|
layering_policy = self._create_layering_policy()
|
||||||
|
layering_policy['metadata']['name'] = 'another-layering-policy'
|
||||||
|
self.assertRaises(errors.SingletonDocumentConflict,
|
||||||
|
self._create_layering_policy)
|
@ -12,7 +12,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
# TODO(fmontei): Make all these version-less.
|
|
||||||
DOCUMENT_SCHEMA_TYPES = (
|
DOCUMENT_SCHEMA_TYPES = (
|
||||||
CERTIFICATE_SCHEMA,
|
CERTIFICATE_SCHEMA,
|
||||||
CERTIFICATE_KEY_SCHEMA,
|
CERTIFICATE_KEY_SCHEMA,
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
Deckhand will allow only one document with the schema ``LayeringPolicy``
|
||||||
|
to exist in the system at a time. To update the existing layering policy,
|
||||||
|
the layerign policy with the same name as the existing one should be
|
||||||
|
passed in. To create a new layering policy, delete the existing one first.
|
Loading…
Reference in New Issue
Block a user