diff --git a/deckhand/engine/layering.py b/deckhand/engine/layering.py index 21abbcfd..67311721 100644 --- a/deckhand/engine/layering.py +++ b/deckhand/engine/layering.py @@ -265,7 +265,8 @@ class DocumentLayering(object): details='The following pre-validation errors occurred ' '(schema, name, error): %s.' % val_errors) - def __init__(self, documents, substitution_sources=None, validate=True): + def __init__(self, documents, substitution_sources=None, validate=True, + fail_on_missing_sub_src=True): """Contructor for ``DocumentLayering``. :param layering_policy: The document with schema @@ -280,6 +281,9 @@ class DocumentLayering(object): :param validate: Whether to pre-validate documents using built-in schema validation. Default is True. :type validate: bool + :param fail_on_missing_sub_src: Whether to fail on a missing + substitution source. Default is True. + :type fail_on_missing_sub_src: bool :raises LayeringPolicyNotFound: If no LayeringPolicy was found among list of ``documents``. @@ -349,7 +353,8 @@ class DocumentLayering(object): self._calc_all_document_children() self._substitution_sources = substitution_sources or [] self.secrets_substitution = secrets_manager.SecretsSubstitution( - self._substitution_sources) + self._substitution_sources, + fail_on_missing_sub_src=fail_on_missing_sub_src) del self._documents_by_layer del self._documents_by_labels diff --git a/deckhand/engine/secrets_manager.py b/deckhand/engine/secrets_manager.py index 40bb5461..7e68248b 100644 --- a/deckhand/engine/secrets_manager.py +++ b/deckhand/engine/secrets_manager.py @@ -105,7 +105,8 @@ class SecretsManager(object): class SecretsSubstitution(object): """Class for document substitution logic for YAML files.""" - def __init__(self, substitution_sources=None): + def __init__(self, substitution_sources=None, + fail_on_missing_sub_src=True): """SecretSubstitution constructor. This class will automatically detect documents that require @@ -115,9 +116,12 @@ class SecretsSubstitution(object): :param substitution_sources: List of documents that are potential sources for substitution. Should only include concrete documents. :type substitution_sources: List[dict] + :param bool fail_on_missing_sub_src: Whether to fail on a missing + substitution source. Default is True. """ self._substitution_sources = {} + self._fail_on_missing_sub_src = fail_on_missing_sub_src for document in substitution_sources: if not isinstance(document, document_wrapper.DocumentDict): @@ -175,11 +179,15 @@ class SecretsSubstitution(object): message = ('Could not find substitution source document ' '[%s] %s among the provided ' '`substitution_sources`.', src_schema, src_name) - LOG.error(message) - raise errors.SubstitutionSourceNotFound( - src_schema=src_schema, src_name=src_name, - document_schema=document.schema, - document_name=document.name) + if self._fail_on_missing_sub_src: + LOG.error(message) + raise errors.SubstitutionSourceNotFound( + src_schema=src_schema, src_name=src_name, + document_schema=document.schema, + document_name=document.name) + else: + LOG.warning(message) + continue # If the data is a dictionary, retrieve the nested secret # via jsonpath_parse, else the secret is the primitive/string diff --git a/deckhand/tests/unit/engine/test_document_layering.py b/deckhand/tests/unit/engine/test_document_layering.py index b779ef5a..89090b3d 100644 --- a/deckhand/tests/unit/engine/test_document_layering.py +++ b/deckhand/tests/unit/engine/test_document_layering.py @@ -14,7 +14,10 @@ import yaml +import mock + from deckhand.engine import layering +from deckhand.engine import secrets_manager from deckhand import errors from deckhand import factories from deckhand.tests.unit import base as test_base @@ -88,6 +91,34 @@ class TestDocumentLayering(test_base.DeckhandTestCase): self.assertEmpty(global_docs) +class TestDocumentLayeringScenarios(TestDocumentLayering): + + @mock.patch.object(secrets_manager, 'LOG', autospec=True) + def test_layering_with_missing_substitution_source_log_warning(self, + m_log): + """Validate that a missing substitution source document fails.""" + mapping = { + "_SITE_SUBSTITUTIONS_1_": [{ + "dest": { + "path": ".c" + }, + "src": { + "schema": "example/Kind/v1", + "name": "nowhere-to-be-found", + "path": "." + } + }] + } + doc_factory = factories.DocumentFactory(2, [1, 1]) + documents = doc_factory.gen_test(mapping, site_abstract=False) + + self._test_layering(documents, site_expected={}, + fail_on_missing_sub_src=False) + self.assertTrue(m_log.warning.called) + self.assertRegex(m_log.warning.mock_calls[0][1][0][0], + r'Could not find substitution source document .*') + + class TestDocumentLayering2Layers(TestDocumentLayering): def test_layering_default_scenario(self):