Sorting/filtering for rendered-documents.
This PS implements sorting and filtering for rendered-documents endpoint, adds additional validations for sorting, filtering and other layering scenarios, and updates rendered-documents and buckets documentation. Layering scenarios added: - Updating the LayeringPolicy with 2 layers in the layerOrder (down from 3) such that the site document should have its parent document recomputed as the global document. - A deletion action layering scenario (DH currently only has merge, replace scenarios in its funcitonal test suite.) Documentation updated: - clarify the access levels for buckets, which has been a source of confusion. - update api-ref documentation for rendered-documents Change-Id: Idb9b42351dfbdf75a19282c8478065e7564cfc26
This commit is contained in:
parent
18999390c7
commit
75d84312de
@ -51,11 +51,8 @@ class RevisionDocumentsResource(api_base.BaseResource):
|
||||
include_encrypted = policy.conditional_authorize(
|
||||
'deckhand:list_encrypted_documents', req.context, do_raise=False)
|
||||
|
||||
order_by = sort_by = None
|
||||
if 'order' in sanitized_params:
|
||||
order_by = sanitized_params.pop('order')
|
||||
if 'sort' in sanitized_params:
|
||||
sort_by = sanitized_params.pop('sort')
|
||||
order_by = sanitized_params.pop('order', None)
|
||||
sort_by = sanitized_params.pop('sort', None)
|
||||
|
||||
filters = sanitized_params.copy()
|
||||
filters['metadata.storagePolicy'] = ['cleartext']
|
||||
@ -70,10 +67,11 @@ class RevisionDocumentsResource(api_base.BaseResource):
|
||||
LOG.exception(six.text_type(e))
|
||||
raise falcon.HTTPNotFound(description=e.format_message())
|
||||
|
||||
sorted_documents = utils.multisort(documents, sort_by, order_by)
|
||||
# Sorts by creation date by default.
|
||||
documents = utils.multisort(documents, sort_by, order_by)
|
||||
|
||||
resp.status = falcon.HTTP_200
|
||||
resp.body = self.view_builder.list(sorted_documents)
|
||||
resp.body = self.view_builder.list(documents)
|
||||
|
||||
|
||||
class RenderedDocumentsResource(api_base.BaseResource):
|
||||
@ -94,7 +92,8 @@ class RenderedDocumentsResource(api_base.BaseResource):
|
||||
|
||||
@policy.authorize('deckhand:list_cleartext_documents')
|
||||
@common.sanitize_params([
|
||||
'schema', 'metadata.name', 'metadata.label', 'status.bucket'])
|
||||
'schema', 'metadata.name', 'metadata.label', 'status.bucket', 'order',
|
||||
'sort'])
|
||||
def on_get(self, req, resp, sanitized_params, revision_id):
|
||||
include_encrypted = policy.conditional_authorize(
|
||||
'deckhand:list_encrypted_documents', req.context, do_raise=False)
|
||||
@ -122,15 +121,23 @@ class RenderedDocumentsResource(api_base.BaseResource):
|
||||
# Filters to be applied post-rendering, because many documents are
|
||||
# involved in rendering. User filters can only be applied once all
|
||||
# documents have been rendered.
|
||||
order_by = sanitized_params.pop('order', None)
|
||||
sort_by = sanitized_params.pop('sort', None)
|
||||
|
||||
user_filters = sanitized_params.copy()
|
||||
user_filters['metadata.layeringDefinition.abstract'] = False
|
||||
final_documents = [
|
||||
|
||||
rendered_documents = [
|
||||
d for d in rendered_documents if utils.deepfilter(
|
||||
d, **user_filters)]
|
||||
|
||||
if sort_by:
|
||||
rendered_documents = utils.multisort(
|
||||
rendered_documents, sort_by, order_by)
|
||||
|
||||
resp.status = falcon.HTTP_200
|
||||
resp.body = self.view_builder.list(final_documents)
|
||||
self._post_validate(final_documents)
|
||||
resp.body = self.view_builder.list(rendered_documents)
|
||||
self._post_validate(rendered_documents)
|
||||
|
||||
def _retrieve_documents_for_rendering(self, revision_id, **filters):
|
||||
"""Retrieve all necessary documents needed for rendering. If a layering
|
||||
|
@ -59,17 +59,15 @@ class RevisionsResource(api_base.BaseResource):
|
||||
@policy.authorize('deckhand:list_revisions')
|
||||
@common.sanitize_params(['tag', 'order', 'sort'])
|
||||
def _list_revisions(self, req, resp, sanitized_params):
|
||||
order_by = sort_by = None
|
||||
if 'order' in sanitized_params:
|
||||
order_by = sanitized_params.pop('order')
|
||||
if 'sort' in sanitized_params:
|
||||
sort_by = sanitized_params.pop('sort')
|
||||
order_by = sanitized_params.pop('order', None)
|
||||
sort_by = sanitized_params.pop('sort', None)
|
||||
|
||||
revisions = db_api.revision_get_all(**sanitized_params)
|
||||
sorted_revisions = utils.multisort(revisions, sort_by, order_by)
|
||||
if sort_by:
|
||||
revisions = utils.multisort(revisions, sort_by, order_by)
|
||||
|
||||
resp.status = falcon.HTTP_200
|
||||
resp.body = self.view_builder.list(sorted_revisions)
|
||||
resp.body = self.view_builder.list(revisions)
|
||||
|
||||
@policy.authorize('deckhand:delete_revisions')
|
||||
def on_delete(self, req, resp):
|
||||
|
@ -39,7 +39,7 @@ class Document(object):
|
||||
"""
|
||||
try:
|
||||
return self._inner['metadata']['layeringDefinition']['abstract']
|
||||
except KeyError:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_schema(self):
|
||||
|
@ -322,4 +322,7 @@ class DocumentLayering(object):
|
||||
if 'children' in doc:
|
||||
del doc['children']
|
||||
|
||||
return [d.to_dict() for d in self.layered_docs]
|
||||
return (
|
||||
[d.to_dict() for d in self.layered_docs] +
|
||||
[self.layering_policy.to_dict()]
|
||||
)
|
||||
|
@ -30,6 +30,13 @@ schema = {
|
||||
'storagePolicy': {
|
||||
'type': 'string',
|
||||
'enum': ['encrypted', 'cleartext']
|
||||
},
|
||||
'layeringDefinition': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'abstract': {'type': 'boolean'}
|
||||
},
|
||||
'additionalProperties': False
|
||||
}
|
||||
},
|
||||
'additionalProperties': False,
|
||||
|
@ -18,6 +18,24 @@ tests:
|
||||
status: 204
|
||||
response_headers: null
|
||||
|
||||
- name: add_bucket_layering
|
||||
desc: |-
|
||||
Create `layeringPolicy` in bucket layering with 3 layers: global, region
|
||||
and site.
|
||||
PUT: /api/v1.0/buckets/layering/documents
|
||||
status: 200
|
||||
data: |-
|
||||
---
|
||||
schema: deckhand/LayeringPolicy/v1
|
||||
metadata:
|
||||
schema: metadata/Control/v1
|
||||
name: layering-policy
|
||||
data:
|
||||
layerOrder:
|
||||
- global
|
||||
- region
|
||||
- site
|
||||
|
||||
- name: add_bucket_a
|
||||
desc: Create documents for bucket a
|
||||
PUT: /api/v1.0/buckets/a/documents
|
||||
@ -33,13 +51,64 @@ tests:
|
||||
- name: verify_layering
|
||||
desc: Check for expected layering
|
||||
GET: /api/v1.0/revisions/$RESPONSE['$.[0].status.revision']/rendered-documents
|
||||
query_parameters:
|
||||
sort:
|
||||
- schema
|
||||
- metadata.name
|
||||
status: 200
|
||||
response_multidoc_jsonpaths:
|
||||
$.`len`: 1
|
||||
$.[*].schema: example/Kind/v1
|
||||
$.[*].metadata.name: site-1234
|
||||
$.[*].metadata.schema: metadata/Document/v1
|
||||
$.[*].data:
|
||||
$.`len`: 3
|
||||
$.[0].schema: deckhand/LayeringPolicy/v1
|
||||
$.[1].schema: example/Kind/v1
|
||||
$.[1].metadata.name: site-with-delete-action
|
||||
$.[1].metadata.schema: metadata/Document/v1
|
||||
$.[1].data: {}
|
||||
$.[2].schema: example/Kind/v1
|
||||
$.[2].metadata.name: site-with-merge-action
|
||||
$.[2].metadata.schema: metadata/Document/v1
|
||||
$.[2].data:
|
||||
a:
|
||||
z: 3
|
||||
b: 4
|
||||
|
||||
- name: update_bucket_layering
|
||||
desc: |-
|
||||
Update `LayeringPolicy` in bucket 'layering', so that it only has 2
|
||||
layers. This validates that, by dropping the middle layer "region",
|
||||
layering is still performed using the global and site documents.
|
||||
PUT: /api/v1.0/buckets/layering/documents
|
||||
status: 200
|
||||
data: |-
|
||||
---
|
||||
schema: deckhand/LayeringPolicy/v1
|
||||
metadata:
|
||||
schema: metadata/Control/v1
|
||||
name: layering-policy
|
||||
data:
|
||||
layerOrder:
|
||||
- global
|
||||
- site
|
||||
|
||||
- name: verify_layering_again
|
||||
desc: Check for expected layering with only global and site layers
|
||||
GET: /api/v1.0/revisions/$RESPONSE['$.[0].status.revision']/rendered-documents
|
||||
query_parameters:
|
||||
sort:
|
||||
- schema
|
||||
- metadata.name
|
||||
status: 200
|
||||
response_multidoc_jsonpaths:
|
||||
$.`len`: 3
|
||||
$.[0].schema: deckhand/LayeringPolicy/v1
|
||||
$.[1].schema: example/Kind/v1
|
||||
$.[1].metadata.name: site-with-delete-action
|
||||
$.[1].metadata.schema: metadata/Document/v1
|
||||
$.[1].data: {}
|
||||
$.[2].schema: example/Kind/v1
|
||||
$.[2].metadata.name: site-with-merge-action
|
||||
$.[2].metadata.schema: metadata/Document/v1
|
||||
$.[2].data:
|
||||
a:
|
||||
x: 1
|
||||
y: 2
|
||||
b: 4
|
||||
|
@ -27,13 +27,16 @@ tests:
|
||||
- name: verify_layering_2_layers
|
||||
desc: Check for expected layering with 2 layers
|
||||
GET: /api/v1.0/revisions/$RESPONSE['$.[0].status.revision']/rendered-documents
|
||||
query_parameters:
|
||||
sort: schema
|
||||
status: 200
|
||||
response_multidoc_jsonpaths:
|
||||
$.`len`: 1
|
||||
$.[*].schema: example/Kind/v1
|
||||
$.[*].metadata.name: site-1234
|
||||
$.[*].metadata.schema: metadata/Document/v1
|
||||
$.[*].data:
|
||||
$.`len`: 2
|
||||
$.[0].schema: deckhand/LayeringPolicy/v1
|
||||
$.[1].schema: example/Kind/v1
|
||||
$.[1].metadata.name: site-1234
|
||||
$.[1].metadata.schema: metadata/Document/v1
|
||||
$.[1].data:
|
||||
a:
|
||||
x: 1
|
||||
y: 2
|
||||
@ -54,13 +57,16 @@ tests:
|
||||
- name: verify_layering_3_layers
|
||||
desc: Check for expected layering with 3 layers
|
||||
GET: /api/v1.0/revisions/$RESPONSE['$.[0].status.revision']/rendered-documents
|
||||
query_parameters:
|
||||
sort: schema
|
||||
status: 200
|
||||
response_multidoc_jsonpaths:
|
||||
$.`len`: 1
|
||||
$.[*].schema: example/Kind/v1
|
||||
$.[*].metadata.name: site-1234
|
||||
$.[*].metadata.schema: metadata/Document/v1
|
||||
$.[*].data:
|
||||
$.`len`: 2
|
||||
$.[0].schema: deckhand/LayeringPolicy/v1
|
||||
$.[1].schema: example/Kind/v1
|
||||
$.[1].metadata.name: site-1234
|
||||
$.[1].metadata.schema: metadata/Document/v1
|
||||
$.[1].data:
|
||||
a:
|
||||
z: 3
|
||||
b: 4
|
||||
|
@ -1,14 +1,4 @@
|
||||
---
|
||||
schema: deckhand/LayeringPolicy/v1
|
||||
metadata:
|
||||
schema: metadata/Control/v1
|
||||
name: layering-policy
|
||||
data:
|
||||
layerOrder:
|
||||
- global
|
||||
- region
|
||||
- site
|
||||
---
|
||||
schema: example/Kind/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
|
@ -20,7 +20,7 @@ data:
|
||||
schema: example/Kind/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: site-1234
|
||||
name: site-with-merge-action
|
||||
labels:
|
||||
foo: bar
|
||||
baz: qux
|
||||
@ -33,4 +33,18 @@ metadata:
|
||||
path: .
|
||||
data:
|
||||
b: 4
|
||||
---
|
||||
schema: example/Kind/v1
|
||||
metadata:
|
||||
schema: metadata/Document/v1
|
||||
name: site-with-delete-action
|
||||
layeringDefinition:
|
||||
layer: site
|
||||
parentSelector:
|
||||
key1: value1
|
||||
actions:
|
||||
- method: delete
|
||||
path: .a
|
||||
# No data needed here, since we are deleting, not adding anything.
|
||||
data: {}
|
||||
...
|
||||
|
@ -22,6 +22,7 @@ from deckhand import errors
|
||||
from deckhand import factories
|
||||
from deckhand.tests import test_utils
|
||||
from deckhand.tests.unit.control import base as test_base
|
||||
from deckhand import types
|
||||
|
||||
|
||||
class TestRenderedDocumentsController(test_base.BaseControllerTest):
|
||||
@ -54,6 +55,12 @@ class TestRenderedDocumentsController(test_base.BaseControllerTest):
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
rendered_documents = list(yaml.safe_load_all(resp.text))
|
||||
# TODO(fmontei): Implement "negative" filter server-side.
|
||||
rendered_documents = [
|
||||
d for d in rendered_documents
|
||||
if not d['schema'].startswith(types.LAYERING_POLICY_SCHEMA)
|
||||
]
|
||||
|
||||
self.assertEqual(1, len(rendered_documents))
|
||||
is_abstract = rendered_documents[0]['metadata']['layeringDefinition'][
|
||||
'abstract']
|
||||
@ -104,6 +111,12 @@ class TestRenderedDocumentsController(test_base.BaseControllerTest):
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
rendered_documents = list(yaml.safe_load_all(resp.text))
|
||||
# TODO(fmontei): Implement "negative" filter server-side.
|
||||
rendered_documents = [
|
||||
d for d in rendered_documents
|
||||
if not d['schema'].startswith(types.LAYERING_POLICY_SCHEMA)
|
||||
]
|
||||
|
||||
self.assertEqual(1, len(rendered_documents))
|
||||
self.assertEqual(new_name, rendered_documents[0]['metadata']['name'])
|
||||
self.assertEqual(2, rendered_documents[0]['status']['revision'])
|
||||
@ -231,6 +244,55 @@ class TestRenderedDocumentsControllerNegativeRBAC(
|
||||
# Verify that the created document was not returned.
|
||||
resp = self.app.simulate_get(
|
||||
'/api/v1.0/revisions/%s/rendered-documents' % revision_id,
|
||||
headers={'Content-Type': 'application/x-yaml'})
|
||||
headers={'Content-Type': 'application/x-yaml'},
|
||||
params={'schema': encrypted_document['schema']})
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEmpty(list(yaml.safe_load_all(resp.text)))
|
||||
|
||||
|
||||
class TestRenderedDocumentsControllerSorting(test_base.BaseControllerTest):
|
||||
|
||||
def test_rendered_documents_sorting_metadata_name(self):
|
||||
rules = {'deckhand:list_cleartext_documents': '@',
|
||||
'deckhand:list_encrypted_documents': '@',
|
||||
'deckhand:create_cleartext_documents': '@'}
|
||||
self.policy.set_rules(rules)
|
||||
|
||||
documents_factory = factories.DocumentFactory(2, [1, 1])
|
||||
documents = documents_factory.gen_test({}, global_abstract=False,
|
||||
region_abstract=False, site_abstract=False)
|
||||
expected_names = ['bar', 'baz', 'foo']
|
||||
for idx in range(len(documents)):
|
||||
documents[idx]['metadata']['name'] = expected_names[idx]
|
||||
|
||||
resp = self.app.simulate_put(
|
||||
'/api/v1.0/buckets/mop/documents',
|
||||
headers={'Content-Type': 'application/x-yaml'},
|
||||
body=yaml.safe_dump_all(documents))
|
||||
self.assertEqual(200, resp.status_code)
|
||||
revision_id = list(yaml.safe_load_all(resp.text))[0]['status'][
|
||||
'revision']
|
||||
|
||||
# Test ascending order.
|
||||
resp = self.app.simulate_get(
|
||||
'/api/v1.0/revisions/%s/rendered-documents' % revision_id,
|
||||
params={'sort': 'metadata.name'}, params_csv=False,
|
||||
headers={'Content-Type': 'application/x-yaml'})
|
||||
self.assertEqual(200, resp.status_code)
|
||||
retrieved_documents = list(yaml.safe_load_all(resp.text))
|
||||
|
||||
self.assertEqual(3, len(retrieved_documents))
|
||||
self.assertEqual(expected_names,
|
||||
[d['metadata']['name'] for d in retrieved_documents])
|
||||
|
||||
# Test descending order.
|
||||
resp = self.app.simulate_get(
|
||||
'/api/v1.0/revisions/%s/rendered-documents' % revision_id,
|
||||
params={'sort': 'metadata.name', 'order': 'desc'},
|
||||
params_csv=False, headers={'Content-Type': 'application/x-yaml'})
|
||||
self.assertEqual(200, resp.status_code)
|
||||
retrieved_documents = list(yaml.safe_load_all(resp.text))
|
||||
|
||||
self.assertEqual(3, len(retrieved_documents))
|
||||
self.assertEqual(list(reversed(expected_names)),
|
||||
[d['metadata']['name'] for d in retrieved_documents])
|
||||
|
@ -16,6 +16,7 @@ from deckhand.engine import layering
|
||||
from deckhand import errors
|
||||
from deckhand import factories
|
||||
from deckhand.tests.unit import base as test_base
|
||||
from deckhand import types
|
||||
|
||||
|
||||
class TestDocumentLayering(test_base.DeckhandTestCase):
|
||||
@ -43,6 +44,9 @@ class TestDocumentLayering(test_base.DeckhandTestCase):
|
||||
# should have a metadata.layeringDefinitionn.layer section.
|
||||
rendered_documents = document_layering.render()
|
||||
for doc in rendered_documents:
|
||||
# No need to validate the LayeringPolicy: it remains unchanged.
|
||||
if doc['schema'].startswith(types.LAYERING_POLICY_SCHEMA):
|
||||
continue
|
||||
layer = doc['metadata']['layeringDefinition']['layer']
|
||||
if layer == 'site':
|
||||
site_docs.append(doc)
|
||||
|
@ -98,7 +98,9 @@ Valid query parameters are the same as for
|
||||
``/revisions/{revision_id}/documents``, minus the paremters in
|
||||
``metadata.layeringDetinition``, which are not supported.
|
||||
|
||||
Raises a 500 Internal Server Error if rendered documents fail schema
|
||||
Raises a ``409 Conflict`` if a ``layeringPolicy`` document could not be found.
|
||||
|
||||
Raises a ``500 Internal Server Error`` if rendered documents fail schema
|
||||
validation.
|
||||
|
||||
GET ``/revisions``
|
||||
|
@ -92,6 +92,18 @@ However, documents can be read across different buckets and used together to
|
||||
render finalized configuration documents, to be consumed by other services like
|
||||
Armada, Drydock, Promenade or Shipyard.
|
||||
|
||||
In other words:
|
||||
|
||||
* Documents can be **read** from any bucket.
|
||||
|
||||
This is useful so that documents from different buckets can be used together
|
||||
for layering and substitution.
|
||||
|
||||
* Documents can only be **written** to by the bucket that owns them.
|
||||
|
||||
This is useful because it offers the concept of ownership to a document in
|
||||
which only the bucket that owns the document can manage it.
|
||||
|
||||
.. todo::
|
||||
|
||||
Deckhand should offer RBAC (Role-Based Access Control) around buckets. This
|
||||
|
@ -27,6 +27,11 @@ Prerequisites
|
||||
postgresql database for unit tests. The DB URL is set up as an environment
|
||||
variable via ``PIFPAF_URL`` which is referenced by Deckhand's unit test suite.
|
||||
|
||||
When running `pifpaf run postgresql` (implicitly called by unit tests below),
|
||||
pifpaf uses `pg_config` which can be installed on Ubuntu via::
|
||||
|
||||
sudo apt-get install libpq-dev -y
|
||||
|
||||
Guide
|
||||
-----
|
||||
|
||||
@ -63,9 +68,18 @@ Functional testing
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
Deckhand requires Docker to run its functional tests. A basic installation
|
||||
guide for Docker for Ubuntu can be found
|
||||
`here <https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/>`_.
|
||||
|
||||
* Docker
|
||||
|
||||
Deckhand requires Docker to run its functional tests. A basic installation
|
||||
guide for Docker for Ubuntu can be found
|
||||
`here <https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/>`_
|
||||
|
||||
* uwsgi
|
||||
|
||||
Can be installed on Ubuntu systems via::
|
||||
|
||||
sudo apt-get install uwsgi -y
|
||||
|
||||
Overview
|
||||
--------
|
||||
@ -91,10 +105,15 @@ The command executes ``tools/functional-tests.sh`` which:
|
||||
6) An HTML report that visualizes the result of the test run is output to
|
||||
``results/index.html``.
|
||||
|
||||
At this time, there are no functional tests for policy enforcement
|
||||
verification. Negative tests will be added at a later date to confirm that
|
||||
a 403 Forbidden is raised for each endpoint that does policy enforcement
|
||||
absent necessary permissions.
|
||||
Note that functional tests can be run concurrently; the flags ``--workers``
|
||||
and ``--threads`` which are passed to ``uwsgi`` can be > 1.
|
||||
|
||||
.. todo::
|
||||
|
||||
At this time, there are no functional tests for policy enforcement
|
||||
verification. Negative tests will be added at a later date to confirm that
|
||||
a 403 Forbidden is raised for each endpoint that does policy enforcement
|
||||
absent necessary permissions.
|
||||
|
||||
CICD
|
||||
----
|
||||
|
@ -197,14 +197,14 @@ if [ -z "$DECKHAND_IMAGE" ]; then
|
||||
|
||||
# Set --workers 2, so that concurrency is always tested.
|
||||
uwsgi \
|
||||
--http :9000 \
|
||||
-w deckhand.cmd \
|
||||
--callable deckhand_callable \
|
||||
--enable-threads \
|
||||
--workers 2 \
|
||||
--threads 1 \
|
||||
-L \
|
||||
--pyargv "--config-file $CONF_DIR/deckhand.conf" &> $STDOUT &
|
||||
--http :9000 \
|
||||
-w deckhand.cmd \
|
||||
--callable deckhand_callable \
|
||||
--enable-threads \
|
||||
--workers 2 \
|
||||
--threads 1 \
|
||||
-L \
|
||||
--pyargv "--config-file $CONF_DIR/deckhand.conf" &
|
||||
else
|
||||
log_section "Running Deckhand via Docker"
|
||||
sudo docker run \
|
||||
|
Loading…
x
Reference in New Issue
Block a user