From 07610993376bda9e5e7ab99ff1e6ae7cba048b6f Mon Sep 17 00:00:00 2001 From: "Carter, Matthew (mc981n)" Date: Tue, 30 Apr 2019 11:43:50 -0500 Subject: [PATCH] Validate existence of "deployment-version" during create configdocs This PS adds funtionality to Shipyard to validate the existence of the Pegleg-generated "deployment-version" document (Pegleg change id: I7919b02d70c9797f689cdad85066d3953b978901) when a user runs create configdocs. This validation only checks the presence of the document (by name and schema) and does not care about the document's other contents. The severity of a failed validation is configurable through the "validations" config section in shipyard.conf. The default severity is "Skip", meaning the validation is not ran at all. Note that with the default configuration of new validation, Shipyard functionality should be unchanged. Change-Id: I754617de81f628a24232e890b12b157ba6731c25 --- charts/shipyard/values.yaml | 4 + doc/source/_static/shipyard.conf.sample | 12 +- .../etc/shipyard/shipyard.conf.sample | 12 +- .../shipyard_airflow/conf/config.py | 17 +- .../control/configdocs/configdocs_api.py | 60 ++++++- .../control/helpers/configdocs_helper.py | 156 ++++++++++++------ .../tests/unit/control/test_configdocs_api.py | 153 +++++++++++++++-- .../unit/control/test_configdocs_helper.py | 143 +++++++++++++++- .../shipyard_client/cli/format_utils.py | 7 +- tools/resources/shipyard.conf | 1 + 10 files changed, 487 insertions(+), 78 deletions(-) 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 842defd4..32396345 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