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
This commit is contained in:
Carter, Matthew (mc981n) 2019-04-30 11:43:50 -05:00
parent 64171aacf4
commit 0761099337
10 changed files with 487 additions and 78 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 '

View File

@ -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):
"""

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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):

View File

@ -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