diff --git a/charts/shipyard/values.yaml b/charts/shipyard/values.yaml index 47535832..38f4c564 100644 --- a/charts/shipyard/values.yaml +++ b/charts/shipyard/values.yaml @@ -430,6 +430,10 @@ conf: # and validates. deployment_strategy_schema: shipyard/DeploymentStrategy/v1 validations: + # Control the severity of the deployment-version document validation + # that Shipyard performs during create configdocs. + # Possible values are Skip, Info, Warning, and Error + deployment_version_create: Skip # Control the severity of the deployment-version document validation # that Shipyard performs during commit configdocs. # Possible values are Skip, Info, Warning, and Error diff --git a/doc/source/_static/shipyard.conf.sample b/doc/source/_static/shipyard.conf.sample index 8a07171c..a4fe7652 100644 --- a/doc/source/_static/shipyard.conf.sample +++ b/doc/source/_static/shipyard.conf.sample @@ -441,8 +441,18 @@ # From shipyard_api # +# Control the severity of the deployment-version validation during create +# configdocs. (string value) +# Possible values: +# Skip - Skip the validation altogether +# Info - Print an Info level message if the validation fails +# Warning - Print a Warning level message if the validation fails +# Error - Return an error when the validation fails and prevent the configdocs +# create from proceeding +#deployment_version_create = Skip + # Control the severity of the deployment-version validation validation during -# commit configdocs. (string value) +# commit configdocs. (string value) # Possible values: # Skip - Skip the validation altogether # Info - Print an Info level message if the validation fails diff --git a/src/bin/shipyard_airflow/etc/shipyard/shipyard.conf.sample b/src/bin/shipyard_airflow/etc/shipyard/shipyard.conf.sample index 8a07171c..a4fe7652 100644 --- a/src/bin/shipyard_airflow/etc/shipyard/shipyard.conf.sample +++ b/src/bin/shipyard_airflow/etc/shipyard/shipyard.conf.sample @@ -441,8 +441,18 @@ # From shipyard_api # +# Control the severity of the deployment-version validation during create +# configdocs. (string value) +# Possible values: +# Skip - Skip the validation altogether +# Info - Print an Info level message if the validation fails +# Warning - Print a Warning level message if the validation fails +# Error - Return an error when the validation fails and prevent the configdocs +# create from proceeding +#deployment_version_create = Skip + # Control the severity of the deployment-version validation validation during -# commit configdocs. (string value) +# commit configdocs. (string value) # Possible values: # Skip - Skip the validation altogether # Info - Print an Info level message if the validation fails diff --git a/src/bin/shipyard_airflow/shipyard_airflow/conf/config.py b/src/bin/shipyard_airflow/shipyard_airflow/conf/config.py index 8a5bf4cc..341cb539 100644 --- a/src/bin/shipyard_airflow/shipyard_airflow/conf/config.py +++ b/src/bin/shipyard_airflow/shipyard_airflow/conf/config.py @@ -330,11 +330,26 @@ SECTIONS = [ name='validations', title='Validation Configurations', options=[ + cfg.StrOpt( + 'deployment_version_create', + default='Skip', + help=('Control the severity of the deployment-version ' + 'validation during create configdocs.'), + ignore_case=True, + choices=[('Skip', 'Skip the validation altogether'), + ('Info', 'Print an Info level message if the ' + 'validation fails'), + ('Warning', 'Print a Warning level message if the ' + 'validation fails'), + ('Error', 'Return an error when the validation fails ' + 'and prevent the configdocs create from ' + 'proceeding')] + ), cfg.StrOpt( 'deployment_version_commit', default='Skip', help=('Control the severity of the deployment-version ' - 'validation validation during commit configdocs. '), + 'validation validation during commit configdocs.'), ignore_case=True, choices=[('Skip', 'Skip the validation altogether'), ('Info', 'Print an Info level message if the ' diff --git a/src/bin/shipyard_airflow/shipyard_airflow/control/configdocs/configdocs_api.py b/src/bin/shipyard_airflow/shipyard_airflow/control/configdocs/configdocs_api.py index ac39f0d5..998aa787 100644 --- a/src/bin/shipyard_airflow/shipyard_airflow/control/configdocs/configdocs_api.py +++ b/src/bin/shipyard_airflow/shipyard_airflow/control/configdocs/configdocs_api.py @@ -24,7 +24,7 @@ from shipyard_airflow.control.api_lock import (api_lock, ApiLockType) from shipyard_airflow.control.base import BaseResource from shipyard_airflow.control.helpers import configdocs_helper from shipyard_airflow.control.helpers.configdocs_helper import ( - ConfigdocsHelper) + ConfigdocsHelper, add_messages_to_validation_status) from shipyard_airflow.errors import ApiError CONF = cfg.CONF @@ -33,6 +33,8 @@ VERSION_VALUES = ['buffer', 'committed', 'last_site_action', 'successful_site_action'] +DEPLOYMENT_DATA_DOC = {'name': CONF.document_info.deployment_version_name, + 'schema': CONF.document_info.deployment_version_schema} class ConfigDocsStatusResource(BaseResource): @@ -155,17 +157,57 @@ class ConfigDocsResource(BaseResource): return helper.get_collection_docs(version, collection_id, cleartext_secrets) + def _validate_deployment_version(self, helper, document_data): + """ + Validate that the received documents include a deployment version doc. + This function should only be called if needed, and will not do any + checking to see if shipyard is configured to skip this check. + Return True if the deployment version doc is present, False otherwise. + """ + LOG.info("Validating deployment data") + LOG.debug("Searching for schema: %s and name: %s", + DEPLOYMENT_DATA_DOC['schema'], DEPLOYMENT_DATA_DOC['name']) + return helper.check_for_document(document_data, + DEPLOYMENT_DATA_DOC['name'], + DEPLOYMENT_DATA_DOC['schema']) + def post_collection(self, helper, collection_id, document_data, buffer_mode_param=None, empty_collection=False): - """ - Ingest the collection after checking preconditions - """ + """Ingest the collection after checking preconditions""" + extra_messages = {'warning': [], 'info': []} + validation_status = None buffer_mode = ConfigdocsHelper.get_buffer_mode(buffer_mode_param) + # Validate that a deployment version document was provided, unless we + # were told to skip this check + ver_validation_cfg = CONF.validations.deployment_version_create.lower() + if empty_collection or ver_validation_cfg == 'skip': + LOG.debug('Skipping deployment version document validation') + else: + if not self._validate_deployment_version(helper, document_data): + title = 'Deployment version document missing from collection' + error_msg = ('Expected document to be present with schema: {} ' + 'and name: {}').format( + DEPLOYMENT_DATA_DOC['schema'], + DEPLOYMENT_DATA_DOC['name']) + + if ver_validation_cfg in ['info', 'warning']: + extra_messages[ver_validation_cfg].append('{}. {}'.format( + title, error_msg)) + else: # Error + raise ApiError( + title=title, + description=('Collection rejected due to missing ' + 'deployment data document'), + status=falcon.HTTP_400, + error_list=[{'message': error_msg}], + retry=False, + ) + if helper.is_buffer_valid_for_bucket(collection_id, buffer_mode): buffer_revision = helper.add_collection(collection_id, document_data) @@ -190,7 +232,8 @@ class ConfigDocsResource(BaseResource): retry=False, ) else: - return helper.get_deckhand_validation_status(buffer_revision) + validation_status = helper.get_deckhand_validation_status( + buffer_revision) else: raise ApiError( title='Invalid collection specified for buffer', @@ -205,6 +248,13 @@ class ConfigDocsResource(BaseResource): retry=False, ) + for level, messages in extra_messages.items(): + if len(messages): + add_messages_to_validation_status(validation_status, + messages, + level) + return validation_status + class CommitConfigDocsResource(BaseResource): """ diff --git a/src/bin/shipyard_airflow/shipyard_airflow/control/helpers/configdocs_helper.py b/src/bin/shipyard_airflow/shipyard_airflow/control/helpers/configdocs_helper.py index b8b86e2a..01e94ce1 100644 --- a/src/bin/shipyard_airflow/shipyard_airflow/control/helpers/configdocs_helper.py +++ b/src/bin/shipyard_airflow/shipyard_airflow/control/helpers/configdocs_helper.py @@ -19,6 +19,7 @@ bucket for Shipyard import enum import logging import threading +import yaml import falcon from oslo_config import cfg @@ -57,25 +58,21 @@ ROLLBACK_COMMIT = 'rollback_commit' class BufferMode(enum.Enum): - """ - Enumeration of the valid values for BufferMode - """ + """Enumeration of the valid values for BufferMode""" REJECTONCONTENTS = 'rejectoncontents' APPEND = 'append' REPLACE = 'replace' class ConfigdocsHelper(object): - """ - ConfigdocsHelper provides a layer to represent the buffer and committed + """ConfigdocsHelper provides a layer to represent the buffer and committed versions of design documents. A new configdocs_helper is intended to be used for each invocation of the service. """ def __init__(self, context): - """ - Sets up this Configdocs helper with the supplied + """Sets up this Configdocs helper with the supplied request context """ self.deckhand = DeckhandClient(context.request_id, @@ -125,9 +122,7 @@ class ConfigdocsHelper(object): return True def is_collection_in_buffer(self, collection_id): - """ - Returns if the collection is represented in the buffer - """ + """Returns if the collection is represented in the buffer""" if self.is_buffer_empty(): return False @@ -152,8 +147,7 @@ class ConfigdocsHelper(object): retry=False, ) def is_buffer_valid_for_bucket(self, collection_id, buffermode): - """ - Indicates if the buffer as it currently is, may be written to + """Indicates if the buffer as it currently is, may be written to for the specified collection, based on the buffermode. """ # can always write if buffer is empty. @@ -180,6 +174,52 @@ class ConfigdocsHelper(object): self.deckhand.rollback(committed_rev_id) return True + def parse_received_doc_data(self, document_data): + """Parse and return the document data shipyard receives + document_data should be a "bytes" type of one or more yaml documents + Return the parsed documents as a list. If bad YAML was provided log a + warning and return an empty list + """ + try: + yaml_doc_list = list(yaml.safe_load_all(document_data)) + LOG.debug('Loaded %s YAML documents from provided data', + len(yaml_doc_list)) + except yaml.YAMLError as exc: + yaml_doc_list = [] + LOG.warning(('Invalid YAML provided to Shipyard. Syntax error(s): ' + '{}').format(exc)) + + return yaml_doc_list + + def get_doc_names_and_schemas(self, document_data): + """Given the document_data shipyard receives, return a list of tuples + denoting each documents' name and schema (name, schema) + """ + parsed_docs = self.parse_received_doc_data(document_data) + names_and_schemas = [] + + for doc in parsed_docs: + try: + schema = doc['schema'] + except (TypeError, KeyError): + schema = '' + LOG.warning('Document recevied with no schema') + try: + name = doc['metadata']['name'] + except (TypeError, KeyError): + name = '' + LOG.warning('Document recevied with no name') + + names_and_schemas.append((name, schema)) + return names_and_schemas + + def check_for_document(self, document_data, name, schema): + """Given the document data shipyard recevies, see if the given + name/schmea combination exists in the list of documents + Return True if the document exists, False otherwise + """ + return (name, schema) in self.get_doc_names_and_schemas(document_data) + def get_configdocs_status(self, versions=None): """ :param versions: A list of 2 versions. Defaults to buffer and @@ -244,8 +284,7 @@ class ConfigdocsHelper(object): return configdocs_status def _get_revision_dict(self): - """ - Returns a dictionary with values representing the revisions in + """Returns a dictionary with values representing the revisions in Deckhand that Shipyard cares about - committed, buffer, latest, last_site_action and successful_site_action, as well as a count of revisions. @@ -326,7 +365,7 @@ class ConfigdocsHelper(object): return self.revision_dict def _get_revision(self, target_revision): - # Helper to drill down to the target revision + """Helper to drill down to the target revision""" return self._get_revision_dict().get(target_revision) def get_revision_id(self, target_revision): @@ -336,8 +375,7 @@ class ConfigdocsHelper(object): def get_collection_docs(self, version, collection_id, cleartext_secrets=False): - """ - Returns the requested collection of docs based on the version + """Returns the requested collection of docs based on the version specifier. The default is set as buffer. """ LOG.info('Retrieving collection %s from %s', collection_id, version) @@ -349,8 +387,7 @@ class ConfigdocsHelper(object): cleartext_secrets=cleartext_secrets) def _get_doc_from_buffer(self, collection_id, cleartext_secrets=False): - """ - Returns the collection if it exists in the buffer. + """Returns the collection if it exists in the buffer. If the buffer contains the collection, the latest representation is what we want. """ @@ -373,8 +410,7 @@ class ConfigdocsHelper(object): def _get_target_docs(self, collection_id, target_rev, cleartext_secrets=False): - """ - Returns the collection if it exists as committed, last_site_action + """Returns the collection if it exists as committed, last_site_action or successful_site_action. """ revision_id = self.get_revision_id(target_rev) @@ -392,8 +428,7 @@ class ConfigdocsHelper(object): retry=False) def get_rendered_configdocs(self, version=BUFFER, cleartext_secrets=False): - """ - Returns the rendered configuration documents for the specified + """Returns the rendered configuration documents for the specified revision (by name BUFFER, COMMITTED, LAST_SITE_ACTION, SUCCESSFUL_SITE_ACTION) """ @@ -519,7 +554,7 @@ class ConfigdocsHelper(object): return _format_validations_to_status(resp_msgs, error_count) def _get_shipyard_validations(self, revision_id): - # Run Shipyard's own validations. + """Run Shipyard's own validations.""" try: sy_val_mgr = DocumentValidationManager( service_clients.deckhand_client(), @@ -564,9 +599,7 @@ class ConfigdocsHelper(object): return resp_msgs def tag_buffer(self, tag): - """ - Convenience method to tag the buffer version. - """ + """Convenience method to tag the buffer version.""" buffer_rev_id = self.get_revision_id(BUFFER) if buffer_rev_id is None: raise AppError( @@ -578,14 +611,11 @@ class ConfigdocsHelper(object): self.tag_revision(buffer_rev_id, tag) def tag_revision(self, revision_id, tag): - """ - Tags the specified revision with the specified tag - """ + """Tags the specified revision with the specified tag""" self.deckhand.tag_revision(revision_id=revision_id, tag=tag) def add_collection(self, collection_id, document_string): - """ - Triggers a call to Deckhand to add a collection(bucket) + """Triggers a call to Deckhand to add a collection(bucket) Documents are assumed to be a string input, not a collection. Returns the id of the buffer version. @@ -653,7 +683,7 @@ class ConfigdocsHelper(object): return False def _get_ordered_versions(self, versions=None): - """returns a list of ordered versions""" + """Returns a list of ordered versions""" # Default ordering def_order = [SUCCESSFUL_SITE_ACTION, @@ -744,8 +774,30 @@ class ConfigdocsHelper(object): # functions for module. # +def add_messages_to_validation_status(status, msgs, level): + """Given a status retrieved from _format_validations_to_status and a list + of messages at a given level (Error, Warning, Info), add messages to the + status + """ + code = falcon.HTTP_200 + if str(level).lower() == 'error': + code = falcon.HTTP_400 + status['status'] = 'Failure' + status['message'] = 'Validations failed' + status['code'] = code + status['details']['errorCount'] += len(msgs) + + formatted_messages = [] + for msg in msgs: + formatted_messages.append({'code': code, + 'message': msg, + 'status': str(level).capitalize(), + 'level': str(level).lower()}) + status['details']['messageList'] += formatted_messages + + def _get_validation_endpoints(): - # returns the list of validation endpoint supported + """Returns the list of validation endpoint supported""" val_ep = '{}/validatedesign' return [ { @@ -764,7 +816,7 @@ def _get_validation_endpoints(): def _get_validation_threads(validation_endpoints, ctx, design_ref): - # create a list of validation threads from the endpoints + """Create a list of validation threads from the endpoints""" validation_threads = [] for endpoint in validation_endpoints: # create a holder for things we need back from the threads @@ -798,7 +850,7 @@ def _get_validation_threads(validation_endpoints, ctx, design_ref): def _get_validations_for_component(url, design_reference, response, exception, context_marker, thread_name, **kwargs): - # Invoke the POST for validation + """Invoke the POST for validation""" try: headers = { 'X-Context-Marker': context_marker, @@ -840,11 +892,12 @@ def _get_validations_for_component(url, design_reference, response, def _generate_dh_val_msg(msg, dh_result_name): - # Maps a deckhand validation response to a ValidationMessage. - # Result name is used if the msg doesn't specify a name field. - # Deckhand may provide the following fields: - # 'validation_schema', 'schema_path', 'name', 'schema', 'path', - # 'error_section', 'message' + """Maps a deckhand validation response to a ValidationMessage. + Result name is used if the msg doesn't specify a name field. + Deckhand may provide the following fields: + 'validation_schema', 'schema_path', 'name', 'schema', 'path', + 'error_section', 'message' + """ not_spec = 'not specified' if 'diagnostic' not in msg: # format path, error_section, validation_schema, and schema_path @@ -871,12 +924,13 @@ def _generate_dh_val_msg(msg, dh_result_name): def _generate_validation_message(msg, **kwargs): - # Special note about kwargs: the values provided via kwargs are used - # as defaults, not overrides. Values in the msg will take precedence. - # - # Using a compatible message, transform it into a ValidationMessage. - # By combining it with the default values passed via kwargs. The values - # used from kwargs match the fields listed below. + """Special note about kwargs: the values provided via kwargs are used + as defaults, not overrides. Values in the msg will take precedence. + + Using a compatible message, transform it into a ValidationMessage. + By combining it with the default values passed via kwargs. The values + used from kwargs match the fields listed below. + """ fields = ['message', 'error', 'name', 'documents', 'level', 'diagnostic', 'source'] @@ -894,7 +948,7 @@ def _generate_validation_message(msg, **kwargs): def _error_to_level(error): - """Convert a boolean error field to 'Error' or 'Info' """ + """Convert a boolean error field to 'Error' or 'Info'""" if error: return 'Error' else: @@ -902,9 +956,9 @@ def _error_to_level(error): def _format_validations_to_status(val_msgs, error_count): - # Using a list of validation messages and an error count, - # formulates and returns a status response dict - + """Using a list of validation messages and an error count, + formulates and returns a status response dict + """ status = 'Success' message = 'Validations succeeded' code = falcon.HTTP_200 diff --git a/src/bin/shipyard_airflow/tests/unit/control/test_configdocs_api.py b/src/bin/shipyard_airflow/tests/unit/control/test_configdocs_api.py index 969d4170..519dc150 100644 --- a/src/bin/shipyard_airflow/tests/unit/control/test_configdocs_api.py +++ b/src/bin/shipyard_airflow/tests/unit/control/test_configdocs_api.py @@ -16,12 +16,14 @@ import json from unittest import mock from unittest.mock import ANY, patch +from oslo_config import cfg import pytest from shipyard_airflow.control.base import ShipyardRequestContext from shipyard_airflow.control.configdocs.configdocs_api import ( CommitConfigDocsResource, - ConfigDocsResource + ConfigDocsResource, + DEPLOYMENT_DATA_DOC ) from shipyard_airflow.control.helpers import configdocs_helper from shipyard_airflow.control.helpers.configdocs_helper import \ @@ -31,6 +33,7 @@ from shipyard_airflow.errors import ApiError from tests.unit.control import common CTX = ShipyardRequestContext() +CONF = cfg.CONF class TestConfigDocsStatusResource(): @@ -88,6 +91,21 @@ class TestConfigDocsResource(): # should not raise an exception. assert False + def test__validate_deployment_version(self): + """Test of the validate deployment version function + """ + helper = None + with patch.object( + ConfigdocsHelper, 'check_for_document') as mock_method: + cdr = ConfigDocsResource() + helper = ConfigdocsHelper(CTX) + cdr._validate_deployment_version(helper, 'oranges') + + mock_method.assert_called_once_with('oranges', + DEPLOYMENT_DATA_DOC['name'], + DEPLOYMENT_DATA_DOC['schema']) + + def test_get_collection(self): helper = None with patch.object( @@ -100,21 +118,22 @@ class TestConfigDocsResource(): @patch.object(ConfigdocsHelper, 'is_collection_in_buffer', lambda x, y: True) + @patch.object(ConfigdocsHelper, 'is_buffer_valid_for_bucket', + lambda x, y, z: True) + @patch.object(ConfigdocsHelper, 'get_deckhand_validation_status', + lambda x, y: configdocs_helper. + _format_validations_to_status([], 0)) def test_post_collection(self): """ Tests the post collection method of the ConfigdocsResource """ + CONF.set_override('deployment_version_create', 'Skip', 'validations') helper = None collection_id = 'trees' document_data = 'lots of info' with patch.object(ConfigdocsHelper, 'add_collection') as mock_method: cdr = ConfigDocsResource() helper = ConfigdocsHelper(CTX) - helper.is_buffer_valid_for_bucket = lambda a, b: True - helper.get_deckhand_validation_status = ( - lambda a: configdocs_helper._format_validations_to_status([], - 0) - ) cdr.post_collection(helper=helper, collection_id=collection_id, document_data=document_data) @@ -125,10 +144,14 @@ class TestConfigDocsResource(): lambda x, y: True) @patch.object(ConfigdocsHelper, 'is_buffer_valid_for_bucket', lambda x, y, z: False) + @patch.object(ConfigdocsHelper, 'get_deckhand_validation_status', + lambda x, y: configdocs_helper. + _format_validations_to_status([], 0)) def test_post_collection_not_valid_for_buffer(self): """ Tests the post collection method of the ConfigdocsResource """ + CONF.set_override('deployment_version_create', 'Skip', 'validations') helper = None collection_id = 'trees' document_data = 'lots of info' @@ -136,10 +159,6 @@ class TestConfigDocsResource(): cdr = ConfigDocsResource() helper = ConfigdocsHelper(CTX) # not valid for bucket - helper.get_deckhand_validation_status = ( - lambda a: configdocs_helper._format_validations_to_status([], - 0) - ) cdr.post_collection(helper=helper, collection_id=collection_id, document_data=document_data) @@ -149,28 +168,134 @@ class TestConfigDocsResource(): lambda x, y: False) @patch.object(ConfigdocsHelper, 'is_buffer_valid_for_bucket', lambda x, y, z: True) + @patch.object(ConfigdocsHelper, 'get_deckhand_validation_status', + lambda x, y: configdocs_helper. + _format_validations_to_status([], 0)) def test_post_collection_not_added(self): """ Tests the post collection method of the ConfigdocsResource """ + CONF.set_override('deployment_version_create', 'Skip', 'validations') helper = None collection_id = 'trees' document_data = 'lots of info' with patch.object(ConfigdocsHelper, 'add_collection') as mock_method: cdr = ConfigDocsResource() helper = ConfigdocsHelper(CTX) - helper.get_deckhand_validation_status = ( - lambda a: configdocs_helper._format_validations_to_status([], - 0) - ) with pytest.raises(ApiError) as apie: cdr.post_collection(helper=helper, collection_id=collection_id, document_data=document_data) assert apie.value.status == '400 Bad Request' + assert apie.value.title == ('Collection {} not added to Shipyard ' + 'buffer'.format(collection_id)) mock_method.assert_called_once_with(collection_id, document_data) + def test_post_collection_deployment_version_missing_error(self): + """ + Tests the post collection method of the ConfigdocsResource + """ + # Make sure that the configuration value is handled case-insensitively + CONF.set_override('deployment_version_create', 'eRRoR', 'validations') + helper = None + collection_id = 'trees' + document_data = 'lots of info' + cdr = ConfigDocsResource() + helper = ConfigdocsHelper(CTX) + with pytest.raises(ApiError) as apie: + cdr.post_collection(helper=helper, + collection_id=collection_id, + document_data=document_data) + + assert apie.value.status == '400 Bad Request' + assert apie.value.title == ('Deployment version document missing from ' + 'collection') + + @patch.object(ConfigdocsHelper, 'is_collection_in_buffer', + lambda x, y: True) + @patch.object(ConfigdocsHelper, 'is_buffer_valid_for_bucket', + lambda x, y, z: True) + @patch.object(ConfigdocsHelper, 'get_deckhand_validation_status', + lambda x, y: configdocs_helper. + _format_validations_to_status([], 0)) + def test_post_collection_deployment_version_missing_warning(self): + """ + Tests the post collection method of the ConfigdocsResource + """ + # Make sure that the configuration value is handled case-insensitively + CONF.set_override('deployment_version_create', 'warnING', + 'validations') + helper = None + collection_id = 'trees' + document_data = 'lots of info' + with patch.object(ConfigdocsHelper, 'add_collection') as mock_method: + cdr = ConfigDocsResource() + helper = ConfigdocsHelper(CTX) + status = cdr.post_collection(helper=helper, + collection_id=collection_id, + document_data=document_data) + assert len(status['details']['messageList']) == 1 + assert status['details']['messageList'][0]['level'] == 'warning' + + mock_method.assert_called_once_with(collection_id, document_data) + + @patch.object(ConfigdocsHelper, 'is_collection_in_buffer', + lambda x, y: True) + @patch.object(ConfigdocsHelper, 'is_buffer_valid_for_bucket', + lambda x, y, z: True) + @patch.object(ConfigdocsHelper, 'get_deckhand_validation_status', + lambda x, y: configdocs_helper. + _format_validations_to_status([], 0)) + def test_post_collection_deployment_version_missing_info(self): + """ + Tests the post collection method of the ConfigdocsResource + """ + # Make sure that the configuration value is handled case-insensitively + CONF.set_override('deployment_version_create', 'iNfO', 'validations') + helper = None + collection_id = 'trees' + document_data = 'lots of info' + with patch.object(ConfigdocsHelper, 'add_collection') as mock_method: + cdr = ConfigDocsResource() + helper = ConfigdocsHelper(CTX) + status = cdr.post_collection(helper=helper, + collection_id=collection_id, + document_data=document_data) + assert len(status['details']['messageList']) == 1 + assert status['details']['messageList'][0]['level'] == 'info' + + mock_method.assert_called_once_with(collection_id, document_data) + + @patch.object(ConfigdocsHelper, 'is_collection_in_buffer', + lambda x, y: True) + @patch.object(ConfigdocsHelper, 'is_buffer_valid_for_bucket', + lambda x, y, z: True) + @patch.object(ConfigdocsHelper, 'get_deckhand_validation_status', + lambda x, y: configdocs_helper. + _format_validations_to_status([], 0)) + def test_post_collection_deployment_version_missing_skip(self): + """ + Tests the post collection method of the ConfigdocsResource + """ + # Make sure that the configuration value is handled case-insensitively + CONF.set_override('deployment_version_create', 'SKip', 'validations') + helper = None + collection_id = 'trees' + document_data = 'lots of info' + with patch.object(ConfigdocsHelper, 'add_collection') as mock_method0,\ + patch.object(ConfigDocsResource, + '_validate_deployment_version') as mock_method1: + cdr = ConfigDocsResource() + helper = ConfigdocsHelper(CTX) + status = cdr.post_collection(helper=helper, + collection_id=collection_id, + document_data=document_data) + assert len(status['details']['messageList']) == 0 + + mock_method0.assert_called_once_with(collection_id, document_data) + mock_method1.assert_not_called + class TestCommitConfigDocsResource(): @mock.patch.object(ApiLock, 'release') diff --git a/src/bin/shipyard_airflow/tests/unit/control/test_configdocs_helper.py b/src/bin/shipyard_airflow/tests/unit/control/test_configdocs_helper.py index 09f51f86..392161aa 100644 --- a/src/bin/shipyard_airflow/tests/unit/control/test_configdocs_helper.py +++ b/src/bin/shipyard_airflow/tests/unit/control/test_configdocs_helper.py @@ -20,11 +20,12 @@ import pytest import responses import yaml +import falcon from .fake_response import FakeResponse from shipyard_airflow.control.base import ShipyardRequestContext from shipyard_airflow.control.helpers import configdocs_helper from shipyard_airflow.control.helpers.configdocs_helper import ( - BufferMode, ConfigdocsHelper) + BufferMode, ConfigdocsHelper, add_messages_to_validation_status) from shipyard_airflow.control.helpers.deckhand_client import ( DeckhandClient, DeckhandResponseError, NoRevisionsExistError) @@ -1057,3 +1058,143 @@ def test_check_intermediate_commit(): assert not helper_no_revs.check_intermediate_commit() assert not helper_no_intermidiate_commits.check_intermediate_commit() assert helper_with_intermidiate_commits.check_intermediate_commit() + +def test_parse_received_doc_data(): + helper = ConfigdocsHelper(CTX) + yaml = """ +--- +document1: + - a + - b + - c +--- +document2: + - 1 + - 2 + - 3 +... +""" + parsed = helper.parse_received_doc_data(yaml) + assert type(parsed) == list + assert len(parsed) == 2 + +def test_parse_received_doc_data_bad_yaml(): + helper = ConfigdocsHelper(CTX) + yaml = "--- asdfjklsemicolon:" + parsed = helper.parse_received_doc_data(yaml) + assert type(parsed) == list + assert len(parsed) == 0 + +def test_get_doc_names_and_schemas(): + helper = ConfigdocsHelper(CTX) + yaml = """ +--- +doc_that_does_not_have_a: name or schema +--- +schema: mycool/Document/v1 +metadata: + schema: metadata/Document/v1 + name: cool-doc + storagePolicy: cleartext + layeringDefinition: + abstract: false + layer: global +data: + hello world +--- +schema: notascool/Document/v1 +metadata: + schema: metadata/Document/v1 + name: average-document + storagePolicy: cleartext + layeringDefinition: + abstract: false + layer: global +data: + goodbye space +... +""" + names_and_schemas = helper.get_doc_names_and_schemas(yaml) + assert type(names_and_schemas) == list + assert len(names_and_schemas) == 3 + assert type(names_and_schemas[0]) == tuple + assert names_and_schemas[0][0] == '' + assert names_and_schemas[0][1] == '' + assert names_and_schemas[1][0] == 'cool-doc' + assert names_and_schemas[1][1] == 'mycool/Document/v1' + assert names_and_schemas[2][0] == 'average-document' + assert names_and_schemas[2][1] == 'notascool/Document/v1' + +def test_check_for_document(): + helper = ConfigdocsHelper(CTX) + yaml = """ +--- +schema: mycool/Document/v1 +metadata: + schema: metadata/Document/v1 + name: cool-doc + storagePolicy: cleartext + layeringDefinition: + abstract: false + layer: global +data: + hello world +... +""" + assert helper.check_for_document(yaml, 'cool-doc', 'mycool/Document/v1') + assert not helper.check_for_document(yaml, 'cool-doc', 'nope') + assert not helper.check_for_document(yaml, 'nope', 'mycool/Document/v1') + assert not helper.check_for_document(yaml, 'nope', 'nope') + +def test_add_messages_to_validation_status(): + helper = ConfigdocsHelper(CTX) + status = { + "kind": "Status", + "apiVersion": "v1.0", + "metadata": {}, + "status": "Success", + "message": "Validations succeeded", + "reason": "Validation", + "details": { + "errorCount": 0, + "messageList": [], + }, + "code": falcon.HTTP_200 + } + info_messages = ['message 1', 'message 2'] + warn_messages = ['message 3'] + error_messages = ['message 4', 'message 5'] + + add_messages_to_validation_status(status, info_messages, 'info') + assert status['details']['errorCount'] == 0 + assert len(status['details']['messageList']) == 2 + assert type(status['details']['messageList'][0]) == dict + assert status['details']['messageList'][0]['code'] == falcon.HTTP_200 + assert status['details']['messageList'][0]['message'] == 'message 1' + assert status['details']['messageList'][0]['status'] == 'Info' + assert status['details']['messageList'][0]['level'] == 'info' + assert status["status"] == "Success" + assert status["message"] == "Validations succeeded" + assert status["code"] == falcon.HTTP_200 + + add_messages_to_validation_status(status, warn_messages, 'warning') + assert status['details']['errorCount'] == 0 + assert len(status['details']['messageList']) == 3 + assert status['details']['messageList'][2]['code'] == falcon.HTTP_200 + assert status['details']['messageList'][2]['message'] == 'message 3' + assert status['details']['messageList'][2]['status'] == 'Warning' + assert status['details']['messageList'][2]['level'] == 'warning' + assert status["status"] == "Success" + assert status["message"] == "Validations succeeded" + assert status["code"] == falcon.HTTP_200 + + add_messages_to_validation_status(status, error_messages, 'error') + assert status['details']['errorCount'] == 2 + assert len(status['details']['messageList']) == 5 + assert status['details']['messageList'][3]['code'] == falcon.HTTP_400 + assert status['details']['messageList'][3]['message'] == 'message 4' + assert status['details']['messageList'][3]['status'] == 'Error' + assert status['details']['messageList'][3]['level'] == 'error' + assert status["status"] == "Failure" + assert status["message"] == "Validations failed" + assert status["code"] == falcon.HTTP_400 \ No newline at end of file diff --git a/src/bin/shipyard_client/shipyard_client/cli/format_utils.py b/src/bin/shipyard_client/shipyard_client/cli/format_utils.py index 0568238f..9680e287 100644 --- a/src/bin/shipyard_client/shipyard_client/cli/format_utils.py +++ b/src/bin/shipyard_client/shipyard_client/cli/format_utils.py @@ -178,11 +178,10 @@ def _format_basic_message(message): Returns a single string with embedded newlines """ + level = str(message.get('level', 'Info')).capitalize() if message.get('error', False): - resp = '\n- Error: {}'.format(message.get('message')) - else: - resp = '\n- Info: {}'.format(message.get('message')) - return resp + level = 'Error' # Force showing "Error" + return '\n- {}: {}'.format(level, message.get('message')) def raw_format_response_handler(response): diff --git a/tools/resources/shipyard.conf b/tools/resources/shipyard.conf index eede00a6..eee3d81d 100644 --- a/tools/resources/shipyard.conf +++ b/tools/resources/shipyard.conf @@ -52,4 +52,5 @@ deployment_strategy_schema = shipyard/DeploymentStrategy/v1 deployment_version_name = deployment-version deployment_version_schema = pegleg/DeploymentData/v1 [validations] +deployment_version_create=Skip deployment_version_commit=Skip \ No newline at end of file