[fix] Parent substitution/layering before replacement
Currently it doesn't seem document replacement works exactly as expected: The parent-replacement document can receive layering and substitution data prior to being replaced. Currently, Deckhand does not account for this scenario. A child-replacement depends on its parent-replacement the same way any child depends on its parent: so that the child layers with its parent only after the parent has received all layering and substitution data. But other documents that depend on the parent-replacement actually depend on the child-replacement instead as the child-replacement replaces its parent. So the dependency chain is: PR -> CR -> anything that layers with PR. A unit and functional test have been added for regression. Co-Authored-By: Felipe Monteiro <felipe.monteiro@att.com> Change-Id: I353393f416aa6e441d84add9ebedcd152944d7e8
This commit is contained in:
parent
a004c7a19e
commit
177675e96f
@ -129,8 +129,6 @@ def _populate_data_with_attributes(jsonpath, data):
|
||||
# Handle case where an object needs to be created.
|
||||
elif path not in d:
|
||||
if '\'' or '\"' in path:
|
||||
LOG.info('Stripping subpath %s of start and end quotes during '
|
||||
'path vivification for jsonpath: %s.', path, jsonpath)
|
||||
path = path.strip('\'').strip('\"')
|
||||
d.setdefault(path, {})
|
||||
d = d.get(path)
|
||||
|
@ -298,14 +298,32 @@ class DocumentLayering(object):
|
||||
"""
|
||||
result = []
|
||||
|
||||
def _get_ancestor(doc, parent_meta):
|
||||
parent = self._documents_by_index.get(parent_meta)
|
||||
# Return the parent's replacement, but if that replacement is the
|
||||
# document itself then return the parent.
|
||||
use_replacement = (
|
||||
parent and parent.has_replacement and
|
||||
parent.replaced_by is not doc
|
||||
)
|
||||
if use_replacement:
|
||||
parent = parent.replaced_by
|
||||
return parent
|
||||
|
||||
g = networkx.DiGraph()
|
||||
for document in self._documents_by_index.values():
|
||||
if document.has_replacement:
|
||||
g.add_edge(document.meta, document.replaced_by.meta)
|
||||
elif document.parent_selector and not document.is_replacement:
|
||||
if document.parent_selector:
|
||||
# NOTE: A child-replacement depends on its parent-replacement
|
||||
# the same way any child depends on its parent: so that the
|
||||
# child layers with its parent only after the parent has
|
||||
# received all layering and substitution data. But other
|
||||
# non-replacement child documents must first wait for the
|
||||
# child-relacement to layer with the parent, so that they
|
||||
# can use the replaced data.
|
||||
parent_meta = self._parents.get(document.meta)
|
||||
if parent_meta:
|
||||
g.add_edge(document.meta, parent_meta)
|
||||
ancestor = _get_ancestor(document, parent_meta)
|
||||
if ancestor:
|
||||
g.add_edge(document.meta, ancestor.meta)
|
||||
|
||||
for sub in document.substitutions:
|
||||
# Retrieve the correct substitution source using
|
||||
@ -591,22 +609,6 @@ class DocumentLayering(object):
|
||||
|
||||
return overall_data
|
||||
|
||||
def _get_parent_or_replacement(self, doc, parent_meta):
|
||||
"""Returns the document's parent or the document's parent's
|
||||
replacement.
|
||||
|
||||
:param DocumentDict doc: Document used to get parent.
|
||||
:param tuple parent_meta: Unique parent identifier.
|
||||
:returns: Parent document or its replacement if the parent has one.
|
||||
:rtype: DocumentDict
|
||||
"""
|
||||
parent = self._documents_by_index.get(parent_meta)
|
||||
# Return the parent's replacement, but if that replacement is the
|
||||
# document itself then return the parent.
|
||||
if parent and parent.has_replacement and parent.replaced_by is not doc:
|
||||
parent = parent.replaced_by
|
||||
return parent
|
||||
|
||||
def render(self):
|
||||
"""Perform layering on the list of documents passed to ``__init__``.
|
||||
|
||||
@ -629,11 +631,14 @@ class DocumentLayering(object):
|
||||
if doc.is_control:
|
||||
continue
|
||||
|
||||
LOG.debug("Rendering document %s:%s:%s", *doc.meta)
|
||||
|
||||
if doc.parent_selector:
|
||||
parent_meta = self._parents.get(doc.meta)
|
||||
|
||||
if parent_meta:
|
||||
parent = self._get_parent_or_replacement(doc, parent_meta)
|
||||
LOG.debug("Using parent %s:%s:%s", *parent_meta)
|
||||
parent = self._documents_by_index[parent_meta]
|
||||
|
||||
if doc.actions:
|
||||
rendered_data = parent
|
||||
@ -652,8 +657,7 @@ class DocumentLayering(object):
|
||||
doc, parent, action)
|
||||
except Exception: # nosec
|
||||
pass
|
||||
if not doc.is_abstract:
|
||||
doc.data = rendered_data.data
|
||||
doc.data = rendered_data.data
|
||||
self.secrets_substitution.update_substitution_sources(
|
||||
doc.schema, doc.name, rendered_data.data)
|
||||
self._documents_by_index[doc.meta] = rendered_data
|
||||
@ -679,10 +683,10 @@ class DocumentLayering(object):
|
||||
if substituted_data:
|
||||
rendered_data = substituted_data[0]
|
||||
# Update the actual document data if concrete.
|
||||
if not doc.is_abstract:
|
||||
doc.data = rendered_data.data
|
||||
self.secrets_substitution.update_substitution_sources(
|
||||
doc.schema, doc.name, rendered_data.data)
|
||||
doc.data = rendered_data.data
|
||||
if not doc.has_replacement:
|
||||
self.secrets_substitution.update_substitution_sources(
|
||||
doc.schema, doc.name, rendered_data.data)
|
||||
self._documents_by_index[doc.meta] = rendered_data
|
||||
# Otherwise, retrieve the encrypted data for the document if its
|
||||
# data has been encrypted so that future references use the actual
|
||||
@ -696,6 +700,13 @@ class DocumentLayering(object):
|
||||
doc.schema, doc.name, encrypted_data)
|
||||
self._documents_by_index[doc.meta] = encrypted_data
|
||||
|
||||
# NOTE: Since the child-replacement is always prioritized, before
|
||||
# other children, as soon as the child-replacement layers with the
|
||||
# parent (which already has undergone layering and substitution
|
||||
# itself), replace the parent data with that of the replacement.
|
||||
if doc.is_replacement:
|
||||
parent.data = doc.data
|
||||
|
||||
# Return only concrete documents and non-replacements.
|
||||
return [d for d in self._sorted_documents
|
||||
if d.is_abstract is False and d.has_replacement is False]
|
||||
|
@ -0,0 +1,51 @@
|
||||
# Tests success path for advanced replacement scenario, where
|
||||
# parent-replacement document receives substitutions and is then layered
|
||||
# with by its child-replacement document, which replaces its parent.
|
||||
#
|
||||
# 1. Purges existing data to ensure test isolation.
|
||||
# 2. Adds initial documents with replacement scenario described above.
|
||||
# 3. Verifies correctly layered, substituted and replaced data.
|
||||
|
||||
defaults:
|
||||
request_headers:
|
||||
content-type: application/x-yaml
|
||||
response_headers:
|
||||
content-type: application/x-yaml
|
||||
verbose: true
|
||||
|
||||
tests:
|
||||
- name: purge
|
||||
desc: Begin testing from known state.
|
||||
DELETE: /api/v1.0/revisions
|
||||
status: 204
|
||||
response_headers: null
|
||||
|
||||
- name: initialize
|
||||
desc: Create initial documents
|
||||
PUT: /api/v1.0/buckets/mop/documents
|
||||
status: 200
|
||||
data: <@resources/replacement.yaml
|
||||
|
||||
- name: verify_replacement_document_receives_substitution
|
||||
desc: |
|
||||
Tests success path for advanced replacement scenario, where
|
||||
parent-replacement document receives substitutions and is then layered
|
||||
with by its child-replacement document, which replaces its parent.
|
||||
GET: /api/v1.0/revisions/$RESPONSE['$.[0].status.revision']/rendered-documents
|
||||
query_parameters:
|
||||
schema: armada/Chart/v1
|
||||
status: 200
|
||||
response_multidoc_jsonpaths:
|
||||
$.`len`: 1
|
||||
$.[*].metadata.name: example-chart-01
|
||||
$.[*].metadata.layeringDefinition.layer: site
|
||||
$.[*].data:
|
||||
chart:
|
||||
details:
|
||||
data: bar
|
||||
values:
|
||||
tls:
|
||||
certificate: |
|
||||
CERTIFICATE DATA
|
||||
key: |
|
||||
KEY DATA
|
76
deckhand/tests/functional/gabbits/resources/replacement.yaml
Normal file
76
deckhand/tests/functional/gabbits/resources/replacement.yaml
Normal file
@ -0,0 +1,76 @@
|
||||
---
|
||||
schema: deckhand/LayeringPolicy/v1
|
||||
metadata:
|
||||
schema: metadata/Control/v1
|
||||
name: layering-policy
|
||||
data:
|
||||
layerOrder:
|
||||
- region
|
||||
- site
|
||||
---
|
||||
schema: deckhand/Certificate/v1
|
||||
metadata:
|
||||
name: example-cert
|
||||
schema: metadata/Document/v1
|
||||
layeringDefinition:
|
||||
layer: site
|
||||
storagePolicy: cleartext
|
||||
data: |
|
||||
CERTIFICATE DATA
|
||||
---
|
||||
schema: deckhand/CertificateKey/v1
|
||||
metadata:
|
||||
name: example-key
|
||||
schema: metadata/Document/v1
|
||||
layeringDefinition:
|
||||
layer: site
|
||||
storagePolicy: cleartext
|
||||
data: |
|
||||
KEY DATA
|
||||
---
|
||||
schema: armada/Chart/v1
|
||||
metadata:
|
||||
name: example-chart-01
|
||||
schema: metadata/Document/v1
|
||||
labels:
|
||||
name: parent-chart
|
||||
layeringDefinition:
|
||||
layer: region
|
||||
substitutions:
|
||||
- dest:
|
||||
path: .chart.values.tls.certificate
|
||||
src:
|
||||
schema: deckhand/Certificate/v1
|
||||
name: example-cert
|
||||
path: .
|
||||
data:
|
||||
chart:
|
||||
details:
|
||||
data: foo
|
||||
values: {}
|
||||
---
|
||||
schema: armada/Chart/v1
|
||||
metadata:
|
||||
name: example-chart-01
|
||||
schema: metadata/Document/v1
|
||||
replacement: true
|
||||
layeringDefinition:
|
||||
layer: site
|
||||
parentSelector:
|
||||
name: parent-chart
|
||||
actions:
|
||||
- method: merge
|
||||
path: .
|
||||
substitutions:
|
||||
- dest:
|
||||
path: .chart.values.tls.key
|
||||
src:
|
||||
schema: deckhand/CertificateKey/v1
|
||||
name: example-key
|
||||
path: .
|
||||
data:
|
||||
chart:
|
||||
details:
|
||||
data: bar
|
||||
values: {}
|
||||
...
|
@ -323,8 +323,9 @@ data:
|
||||
site_expected = 'should not change'
|
||||
self._test_layering(documents, site_expected, global_expected=None)
|
||||
|
||||
error_re = r'^Skipped layering for document.*'
|
||||
self.assertRegex(mock_log.debug.mock_calls[0][1][0], error_re)
|
||||
msg_re = 'Skipped layering for document'
|
||||
mock_calls = [x[1][0] for x in mock_log.debug.mock_calls]
|
||||
self.assertTrue(any(x.startswith(msg_re) for x in mock_calls))
|
||||
|
||||
|
||||
class TestDocumentLayering2Layers(TestDocumentLayering):
|
||||
|
@ -13,6 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import itertools
|
||||
import os
|
||||
import yaml
|
||||
|
||||
from deckhand.tests.unit.engine import test_document_layering
|
||||
@ -106,10 +107,11 @@ data:
|
||||
# The replacer should be used as the source.
|
||||
self._test_layering(self.documents, site_expected,
|
||||
global_expected=global_expected)
|
||||
# Attempt the same scenario but reverse the order of the documents,
|
||||
# which verifies that the replacer always takes priority.
|
||||
self._test_layering(self.documents, site_expected,
|
||||
global_expected=global_expected)
|
||||
# Try different permutations of document orders for good measure.
|
||||
for documents in list(itertools.permutations(self.documents))[:10]:
|
||||
self._test_layering(
|
||||
documents, site_expected=site_expected,
|
||||
global_expected=global_expected)
|
||||
|
||||
def test_replacement_with_layering_with_replacer(self):
|
||||
"""Verify that replacement works alongside layering. This scenario
|
||||
@ -212,3 +214,40 @@ data:
|
||||
self._test_layering(
|
||||
documents, site_expected=site_expected,
|
||||
global_expected=global_expected)
|
||||
|
||||
def test_replacement_document_receives_substitution(self):
|
||||
"""Verifies that the parent-replacement receives substitution data
|
||||
prior to the child-replacement layering with it, which in turn is
|
||||
done prior to any other document attempting to substitute from or
|
||||
layer with the child-replacement (which replaces its parent).
|
||||
"""
|
||||
test_path = os.path.join(
|
||||
os.getcwd(), 'deckhand', 'tests', 'functional', 'gabbits',
|
||||
'resources', 'replacement.yaml')
|
||||
with open(test_path) as f:
|
||||
self.documents = list(yaml.safe_load_all(f))
|
||||
|
||||
site_expected = [
|
||||
"CERTIFICATE DATA\n",
|
||||
"KEY DATA\n",
|
||||
{
|
||||
'chart': {
|
||||
'details': {'data': 'bar'},
|
||||
'values': {
|
||||
'tls': {
|
||||
'certificate': 'CERTIFICATE DATA\n',
|
||||
'key': 'KEY DATA\n'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
self._test_layering(self.documents, site_expected=site_expected,
|
||||
region_expected=None)
|
||||
|
||||
# Try different permutations of document orders for good measure.
|
||||
for documents in list(itertools.permutations(self.documents))[:10]:
|
||||
self._test_layering(
|
||||
documents, site_expected=site_expected,
|
||||
region_expected=None)
|
||||
|
Loading…
x
Reference in New Issue
Block a user