9345035522
GET /revisions/{{revision_id}}/deepdiff/{{comparison_revision_id}} - Added deepdiff api for generating diff between two rendered documents. - Deep diffing for data and metadata - Refactor diff functions - Client update - Added unit testcases - Added funtional testcases - Doc update Change-Id: Ib60fa60a3b33e9125a1595a999272ca595721b38
430 lines
16 KiB
Python
430 lines
16 KiB
Python
# 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 abc
|
|
import copy
|
|
import six
|
|
|
|
from oslo_log import log as logging
|
|
|
|
from deckhand.common import document as document_wrapper
|
|
from deckhand.db.sqlalchemy import api
|
|
from deckhand.tests import test_utils
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
DOCUMENT_TEST_SCHEMA = 'example/Kind/v1'
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class DeckhandFactory(object):
|
|
|
|
@abc.abstractmethod
|
|
def gen_test(self, *args, **kwargs):
|
|
"""Generate an object with randomized values for a test."""
|
|
pass
|
|
|
|
|
|
class DataSchemaFactory(DeckhandFactory):
|
|
"""Class for auto-generating ``DataSchema`` templates for testing."""
|
|
|
|
DATA_SCHEMA_TEMPLATE = {
|
|
"data": {
|
|
"$schema": ""
|
|
},
|
|
"metadata": {
|
|
"schema": "metadata/Control/v1",
|
|
"name": "",
|
|
"labels": {},
|
|
"layeringDefinition": {
|
|
"abstract": True,
|
|
"layer": "site"
|
|
}
|
|
},
|
|
"schema": "deckhand/DataSchema/v1"
|
|
}
|
|
|
|
def __init__(self):
|
|
"""Constructor for ``DataSchemaFactory``.
|
|
|
|
Returns a template whose YAML representation is of the form::
|
|
|
|
---
|
|
schema: deckhand/DataSchema/v1
|
|
metadata:
|
|
schema: metadata/Control/v1
|
|
name: promenade/Node/v1
|
|
labels:
|
|
application: promenade
|
|
data:
|
|
$schema: http://blah
|
|
...
|
|
"""
|
|
|
|
def gen_test(self, metadata_name, data, **metadata_labels):
|
|
data_schema_template = copy.deepcopy(self.DATA_SCHEMA_TEMPLATE)
|
|
|
|
data_schema_template['metadata']['name'] = metadata_name
|
|
data_schema_template['metadata']['labels'] = metadata_labels
|
|
if data:
|
|
data_schema_template['data'] = data
|
|
|
|
return data_schema_template
|
|
|
|
|
|
class DocumentFactory(DeckhandFactory):
|
|
"""Class for auto-generating document templates for testing."""
|
|
|
|
LAYERING_POLICY_TEMPLATE = {
|
|
"data": {
|
|
"layerOrder": []
|
|
},
|
|
"metadata": {
|
|
"name": "placeholder",
|
|
"schema": "metadata/Control/v1",
|
|
"layeringDefinition": {
|
|
"abstract": False,
|
|
"layer": "layer"
|
|
}
|
|
},
|
|
"schema": "deckhand/LayeringPolicy/v1"
|
|
}
|
|
|
|
DOCUMENT_TEMPLATE = {
|
|
"data": {},
|
|
"metadata": {
|
|
"labels": {"": ""},
|
|
"storagePolicy": "cleartext",
|
|
"layeringDefinition": {
|
|
"abstract": False,
|
|
"layer": "layer"
|
|
},
|
|
"name": "",
|
|
"schema": "metadata/Document/v1"
|
|
},
|
|
"schema": DOCUMENT_TEST_SCHEMA
|
|
}
|
|
|
|
def __init__(self, num_layers, docs_per_layer):
|
|
"""Constructor for ``DocumentFactory``.
|
|
|
|
Returns a template whose JSON representation is of the form::
|
|
|
|
[{'data': {'layerOrder': ['global', 'region', 'site']},
|
|
'metadata': {'name': 'layering-policy',
|
|
'schema': 'metadata/Control/v1'},
|
|
'schema': 'deckhand/LayeringPolicy/v1'},
|
|
{'data': {'a': 1, 'b': 2},
|
|
'metadata': {'labels': {'global': 'global1'},
|
|
'layeringDefinition': {'abstract': True,
|
|
'actions': [],
|
|
'layer': 'global',
|
|
'parentSelector': ''},
|
|
'name': 'global1',
|
|
'schema': 'metadata/Document/v1'},
|
|
'schema': 'example/Kind/v1'}
|
|
...
|
|
]
|
|
|
|
:param num_layers: Total number of layers. Only supported values
|
|
include 1, 2 or 3.
|
|
:type num_layers: integer
|
|
:param docs_per_layer: The number of documents to be included per
|
|
layer. For example, if ``num_layers`` is 3, then ``docs_per_layer``
|
|
can be (1, 1, 1) for 1 document for each layer or (1, 2, 3) for 1
|
|
doc for the 1st layer, 2 docs for the 2nd layer, and 3 docs for the
|
|
3rd layer.
|
|
:type docs_per_layer: tuple, list
|
|
:raises TypeError: If ``docs_per_layer`` is not the right type.
|
|
:raises ValueError: If ``num_layers`` is not the right value or isn't
|
|
compatible with ``docs_per_layer``.
|
|
"""
|
|
# Set up the layering definition's layerOrder.
|
|
if num_layers == 1:
|
|
layer_order = ["global"]
|
|
elif num_layers == 2:
|
|
layer_order = ["global", "site"]
|
|
elif num_layers == 3:
|
|
layer_order = ["global", "region", "site"]
|
|
else:
|
|
raise ValueError("'num_layers' must be a value between 1 - 3.")
|
|
self.layering_policy = copy.deepcopy(self.LAYERING_POLICY_TEMPLATE)
|
|
self.layering_policy['metadata']['name'] = test_utils.rand_name(
|
|
'layering-policy')
|
|
self.layering_policy['data']['layerOrder'] = layer_order
|
|
self.layering_policy['metadata']['layeringDefinition'][
|
|
'layer'] = layer_order[0]
|
|
|
|
if not isinstance(docs_per_layer, (list, tuple)):
|
|
raise TypeError("'docs_per_layer' must be a list or tuple "
|
|
"indicating the number of documents per layer.")
|
|
elif not len(docs_per_layer) == num_layers:
|
|
raise ValueError("The number of entries in 'docs_per_layer' must"
|
|
"be equal to the value of 'num_layers'.")
|
|
|
|
for doc_count in docs_per_layer:
|
|
if doc_count < 0:
|
|
raise ValueError(
|
|
"Each entry in 'docs_per_layer' must be >= 1.")
|
|
|
|
self.num_layers = num_layers
|
|
self.docs_per_layer = docs_per_layer
|
|
|
|
def gen_test(self, mapping, site_abstract=True, region_abstract=True,
|
|
global_abstract=True, site_parent_selectors=None):
|
|
"""Generate the document template.
|
|
|
|
Generate the document template based on the arguments passed to
|
|
the constructor and to this function.
|
|
|
|
:param mapping: A list of dictionaries that specify the "data" and
|
|
"actions" parameters for each document. A valid mapping is::
|
|
|
|
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": path}]}
|
|
}
|
|
|
|
Each key must be of the form "_{LAYER_NAME}_{KEY_NAME}_{N}_"
|
|
where:
|
|
|
|
- {LAYER_NAME} is the name of the layer ("global", "region",
|
|
"site")
|
|
- {KEY_NAME} is either "DATA" or "ACTIONS"
|
|
- {N} is the occurrence of the document based on the
|
|
values in ``docs_per_layer``. If ``docs_per_layer`` is
|
|
(1, 2) then _GLOBAL_DATA_1_, _SITE_DATA_1_, _SITE_DATA_2_,
|
|
_SITE_ACTIONS_1_ and _SITE_ACTIONS_2_ must be provided.
|
|
_GLOBAL_ACTIONS_{N}_ is ignored.
|
|
|
|
:type mapping: dict
|
|
:param site_abstract: Whether site layers are abstract/concrete.
|
|
:type site_abstract: boolean
|
|
:param region_abstract: Whether region layers are abstract/concrete.
|
|
:type region_abstract: boolean
|
|
:param global_abstract: Whether global layers are abstract/concrete.
|
|
:type global_abstract: boolean
|
|
:param site_parent_selectors: Override the default parent selector
|
|
for each site. Assuming that ``docs_per_layer`` is (2, 2), for
|
|
example, a valid value is::
|
|
|
|
[{'global': 'global1'}, {'global': 'global2'}]
|
|
|
|
If not specified, each site will default to the first parent.
|
|
:type site_parent_selectors: list
|
|
:returns: Rendered template of the form specified above.
|
|
"""
|
|
rendered_template = [self.layering_policy]
|
|
layer_order = rendered_template[0]['data']['layerOrder']
|
|
|
|
for layer_idx in range(self.num_layers):
|
|
for count in range(self.docs_per_layer[layer_idx]):
|
|
layer_template = copy.deepcopy(self.DOCUMENT_TEMPLATE)
|
|
layer_name = layer_order[layer_idx]
|
|
|
|
layer_template = copy.deepcopy(layer_template)
|
|
|
|
# Set name.
|
|
name_key = "_%s_NAME_%d_" % (layer_name.upper(), count + 1)
|
|
if name_key in mapping:
|
|
layer_template['metadata']['name'] = mapping[name_key]
|
|
else:
|
|
layer_template['metadata']['name'] = "%s%d" % (
|
|
test_utils.rand_name(layer_name), count + 1)
|
|
|
|
# Set schema.
|
|
schema_key = "_%s_SCHEMA_%d_" % (layer_name.upper(), count + 1)
|
|
if schema_key in mapping:
|
|
layer_template['schema'] = mapping[schema_key]
|
|
|
|
# Set layer.
|
|
layer_template['metadata']['layeringDefinition'][
|
|
'layer'] = layer_name
|
|
|
|
# Set labels.
|
|
layer_template['metadata']['labels'] = {layer_name: "%s%d" % (
|
|
layer_name, count + 1)}
|
|
|
|
# Set parentSelector.
|
|
if layer_name == 'site' and site_parent_selectors:
|
|
parent_selector = site_parent_selectors[count]
|
|
layer_template['metadata']['layeringDefinition'][
|
|
'parentSelector'] = parent_selector
|
|
elif layer_idx > 0:
|
|
parent_selector = rendered_template[layer_idx][
|
|
'metadata']['labels']
|
|
layer_template['metadata']['layeringDefinition'][
|
|
'parentSelector'] = parent_selector
|
|
|
|
# Set abstract.
|
|
if layer_name == 'site':
|
|
layer_template['metadata']['layeringDefinition'][
|
|
'abstract'] = site_abstract
|
|
if layer_name == 'region':
|
|
layer_template['metadata']['layeringDefinition'][
|
|
'abstract'] = region_abstract
|
|
if layer_name == 'global':
|
|
layer_template['metadata']['layeringDefinition'][
|
|
'abstract'] = global_abstract
|
|
|
|
# Set data and actions.
|
|
data_key = "_%s_DATA_%d_" % (layer_name.upper(), count + 1)
|
|
actions_key = "_%s_ACTIONS_%d_" % (
|
|
layer_name.upper(), count + 1)
|
|
sub_key = "_%s_SUBSTITUTIONS_%d_" % (
|
|
layer_name.upper(), count + 1)
|
|
|
|
try:
|
|
layer_template['data'] = mapping[data_key]['data']
|
|
except KeyError as e:
|
|
LOG.debug('Could not map %s because it was not found in '
|
|
'the `mapping` dict.', e.args[0])
|
|
|
|
try:
|
|
layer_template['metadata']['layeringDefinition'][
|
|
'actions'] = mapping[actions_key]['actions']
|
|
except KeyError as e:
|
|
LOG.debug('Could not map %s because it was not found in '
|
|
'the `mapping` dict.', e.args[0])
|
|
|
|
try:
|
|
layer_template['metadata']['substitutions'] = mapping[
|
|
sub_key]
|
|
except KeyError as e:
|
|
LOG.debug('Could not map %s because it was not found in '
|
|
'the `mapping` dict.', e.args[0])
|
|
|
|
rendered_template.append(layer_template)
|
|
|
|
return rendered_template
|
|
|
|
|
|
class DocumentSecretFactory(DeckhandFactory):
|
|
"""Class for auto-generating document secrets templates for testing.
|
|
|
|
Returns formats that adhere to the following supported schemas:
|
|
|
|
* deckhand/Certificate/v1
|
|
* deckhand/CertificateKey/v1
|
|
* deckhand/Passphrase/v1
|
|
"""
|
|
|
|
DOCUMENT_SECRET_TEMPLATE = {
|
|
"data": {
|
|
},
|
|
"metadata": {
|
|
"schema": "metadata/Document/v1",
|
|
"name": "",
|
|
"layeringDefinition": {
|
|
"abstract": False,
|
|
"layer": "site"
|
|
},
|
|
"storagePolicy": "",
|
|
},
|
|
"schema": "deckhand/%s/v1"
|
|
}
|
|
|
|
def __init__(self):
|
|
"""Constructor for ``DocumentSecretFactory``.
|
|
|
|
Returns a template whose YAML representation is of the form::
|
|
|
|
---
|
|
schema: deckhand/Certificate/v1
|
|
metadata:
|
|
schema: metadata/Document/v1
|
|
name: application-api
|
|
storagePolicy: cleartext
|
|
data: |-
|
|
-----BEGIN CERTIFICATE-----
|
|
MIIDYDCCAkigAwIBAgIUKG41PW4VtiphzASAMY4/3hL8OtAwDQYJKoZIhvcNAQEL
|
|
...snip...
|
|
P3WT9CfFARnsw2nKjnglQcwKkKLYip0WY2wh3FE7nrQZP6xKNaSRlh6p2pCGwwwH
|
|
HkvVwA==
|
|
-----END CERTIFICATE-----
|
|
...
|
|
"""
|
|
|
|
def gen_test(self, schema, storage_policy, data=None, name=None):
|
|
if data is None:
|
|
data = test_utils.rand_password()
|
|
if name is None:
|
|
name = test_utils.rand_name('document')
|
|
|
|
document_secret_template = copy.deepcopy(self.DOCUMENT_SECRET_TEMPLATE)
|
|
|
|
document_secret_template['metadata']['storagePolicy'] = storage_policy
|
|
document_secret_template['schema'] = (
|
|
document_secret_template['schema'] % schema)
|
|
document_secret_template['data'] = data
|
|
document_secret_template['metadata']['name'] = name
|
|
|
|
return document_secret_template
|
|
|
|
|
|
class RenderedDocumentFactory(DeckhandFactory):
|
|
"""Class for auto-generating Rendered document for testing.
|
|
"""
|
|
RENDERED_DOCUMENT_TEMPLATE = {
|
|
"data": {
|
|
},
|
|
"data_hash": "",
|
|
"metadata": {
|
|
"schema": "metadata/Document/v1",
|
|
"name": "",
|
|
"layeringDefinition": {
|
|
"abstract": False,
|
|
"layer": "site"
|
|
},
|
|
"storagePolicy": "",
|
|
},
|
|
"metadata_hash": "",
|
|
"name": "",
|
|
"schema": "deckhand/%s/v1",
|
|
"status": {
|
|
"bucket": "",
|
|
"revision": ""
|
|
}
|
|
}
|
|
|
|
def __init__(self, bucket, revision):
|
|
"""Constructor for ``RenderedDocumentFactory``.
|
|
"""
|
|
self.doc = []
|
|
self.bucket = bucket
|
|
self.revision = revision
|
|
|
|
def gen_test(self, schema, name, storagePolicy, data, doc_no=1):
|
|
"""Generate Test Rendered Document.
|
|
"""
|
|
for x in range(doc_no):
|
|
rendered_doc = copy.deepcopy(self.RENDERED_DOCUMENT_TEMPLATE)
|
|
rendered_doc['metadata']['storagePolicy'] = storagePolicy
|
|
rendered_doc['metadata']['name'] = name[x]
|
|
rendered_doc['name'] = name[x]
|
|
rendered_doc['schema'] = (
|
|
rendered_doc['schema'] % schema[x])
|
|
rendered_doc['status']['bucket'] = self.bucket
|
|
rendered_doc['status']['revision'] = self.revision
|
|
rendered_doc['data'] = copy.deepcopy(data[x])
|
|
rendered_doc['data_hash'] = api._make_hash(rendered_doc['data'])
|
|
rendered_doc['metadata_hash'] = api._make_hash(
|
|
rendered_doc['metadata'])
|
|
|
|
self.doc.append(rendered_doc)
|
|
|
|
return document_wrapper.DocumentDict.from_list(self.doc)
|