Improved document validation

BREAKING CHANGE: Armada will no longer support
recursive monolithic documents such that a Manifest
fully defines ChartGroups inline and ChartGroups
fully define Charts inline. Only name-based references
to other documents is supported.

- Author document schemas in standalone
  JSON schema files
- Update validation to return all failures available
- Removed unit tests for support of recursive monolithic
  documents

Change-Id: Idb91fa552d3d7a3d7d525609d505fe7380443238
This commit is contained in:
gardlt 2017-09-15 03:45:06 +00:00 committed by Marshall Margenau
parent ebc71ff8ec
commit 3b879fc846
22 changed files with 1054 additions and 417 deletions

View File

@ -18,7 +18,7 @@ import yaml
from armada import api from armada import api
from armada.common import policy from armada.common import policy
from armada.utils.lint import validate_armada_documents from armada.utils.validate import validate_armada_documents
from armada.handlers.document import ReferenceResolver from armada.handlers.document import ReferenceResolver
@ -50,7 +50,7 @@ class Validate(api.BaseResource):
self.logger.debug("Validating set of %d documents." self.logger.debug("Validating set of %d documents."
% len(documents)) % len(documents))
result = validate_armada_documents(documents) result, details = validate_armada_documents(documents)
resp.content_type = 'application/json' resp.content_type = 'application/json'
resp_body = { resp_body = {
@ -58,12 +58,14 @@ class Validate(api.BaseResource):
'apiVersion': 'v1.0', 'apiVersion': 'v1.0',
'metadata': {}, 'metadata': {},
'reason': 'Validation', 'reason': 'Validation',
'details': { 'details': {},
'errorCount': 0,
'messageList': []
},
} }
error_details = [m for m in details if m.get('error', False)]
resp_body['details']['errorCount'] = len(error_details)
resp_body['details']['messageList'] = details
if result: if result:
resp.status = falcon.HTTP_200 resp.status = falcon.HTTP_200
resp_body['status'] = 'Success' resp_body['status'] = 'Success'
@ -74,9 +76,6 @@ class Validate(api.BaseResource):
resp_body['status'] = 'Failure' resp_body['status'] = 'Failure'
resp_body['message'] = 'Armada validations failed' resp_body['message'] = 'Armada validations failed'
resp_body['code'] = 400 resp_body['code'] = 400
resp_body['details']['errorCount'] = 1
resp_body['details']['messageList'].\
append(dict(message='Validation failed.', error=True))
resp.body = json.dumps(resp_body) resp.body = json.dumps(resp_body)
except Exception as ex: except Exception as ex:

View File

@ -16,9 +16,7 @@ import click
import yaml import yaml
from armada.cli import CliAction from armada.cli import CliAction
from armada.utils.lint import validate_armada_documents from armada.utils.validate import validate_armada_documents
from armada.utils.lint import validate_armada_object
from armada.handlers.manifest import Manifest
from armada.handlers.document import ReferenceResolver from armada.handlers.document import ReferenceResolver
@ -64,16 +62,20 @@ class ValidateManifest(CliAction):
for d in doc_data: for d in doc_data:
documents.extend(list(yaml.safe_load_all(d.decode()))) documents.extend(list(yaml.safe_load_all(d.decode())))
manifest_obj = Manifest(documents).get_manifest()
obj_check = validate_armada_object(manifest_obj)
doc_check = validate_armada_documents(documents)
try: try:
if doc_check and obj_check: valid, details = validate_armada_documents(documents)
if valid:
self.logger.info('Successfully validated: %s', self.logger.info('Successfully validated: %s',
self.locations) self.locations)
else:
self.logger.info('Validation failed: %s', self.locations)
for m in details:
self.logger.info('Validation details: %s', str(m))
except Exception: except Exception:
raise Exception('Failed to validate: %s', self.locations) raise Exception('Exception raised during '
'validation: %s', self.locations)
else: else:
if len(self.locations) > 1: if len(self.locations) > 1:
self.logger.error( self.logger.error(
@ -88,4 +90,7 @@ class ValidateManifest(CliAction):
if resp.get('code') == 200: if resp.get('code') == 200:
self.logger.info('Successfully validated: %s', self.locations) self.logger.info('Successfully validated: %s', self.locations)
else: else:
self.logger.error("Failed to validate: %s", self.locations) self.logger.error("Validation failed: %s", self.locations)
for m in resp.get('details', {}).get('messageList', []):
self.logger.info("Validation details: %s", str(m))

View File

@ -15,13 +15,13 @@
from armada.exceptions import base_exception from armada.exceptions import base_exception
class LintException(base_exception.ArmadaBaseException): class ValidateException(base_exception.ArmadaBaseException):
'''Base class for linting exceptions and errors.''' '''Base class for linting exceptions and errors.'''
message = 'An unknown linting error occurred.' message = 'An unknown linting error occurred.'
class InvalidManifestException(LintException): class InvalidManifestException(ValidateException):
''' '''
Exception for invalid manifests. Exception for invalid manifests.
@ -29,28 +29,29 @@ class InvalidManifestException(LintException):
*Coming Soon* *Coming Soon*
''' '''
message = 'Armada manifest invalid.' message = ('Armada manifest(s) failed validation. Details: '
'%(error_messages)s.')
class InvalidChartNameException(LintException): class InvalidChartNameException(ValidateException):
'''Exception that occurs when an invalid filename is encountered.''' '''Exception that occurs when an invalid filename is encountered.'''
message = 'Chart name must be a string.' message = 'Chart name must be a string.'
class InvalidChartDefinitionException(LintException): class InvalidChartDefinitionException(ValidateException):
'''Exception when invalid chart definition is encountered.''' '''Exception when invalid chart definition is encountered.'''
message = 'Invalid chart definition. Chart definition must be array.' message = 'Invalid chart definition. Chart definition must be array.'
class InvalidReleaseException(LintException): class InvalidReleaseException(ValidateException):
'''Exception that occurs when a release is invalid.''' '''Exception that occurs when a release is invalid.'''
message = 'Release needs to be a string.' message = 'Release needs to be a string.'
class InvalidArmadaObjectException(LintException): class InvalidArmadaObjectException(ValidateException):
''' '''
Exception that occurs when an Armada object is not declared. Exception that occurs when an Armada object is not declared.
@ -58,4 +59,5 @@ class InvalidArmadaObjectException(LintException):
*Coming Soon* *Coming Soon*
''' '''
message = 'An Armada object was not declared.' message = ('An Armada object failed internal validation. Details: '
'%(details)s.')

View File

@ -25,11 +25,11 @@ from armada.handlers.override import Override
from armada.handlers.tiller import Tiller from armada.handlers.tiller import Tiller
from armada.exceptions import armada_exceptions from armada.exceptions import armada_exceptions
from armada.exceptions import source_exceptions from armada.exceptions import source_exceptions
from armada.exceptions import lint_exceptions from armada.exceptions import validate_exceptions
from armada.exceptions import tiller_exceptions from armada.exceptions import tiller_exceptions
from armada.utils.release import release_prefix from armada.utils.release import release_prefix
from armada.utils import source from armada.utils import source
from armada.utils import lint from armada.utils import validate
from armada import const from armada import const
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -44,7 +44,7 @@ class Armada(object):
''' '''
def __init__(self, def __init__(self,
file, documents,
disable_update_pre=False, disable_update_pre=False,
disable_update_post=False, disable_update_post=False,
enable_chart_cleanup=False, enable_chart_cleanup=False,
@ -60,7 +60,7 @@ class Armada(object):
''' '''
Initialize the Armada engine and establish a connection to Tiller. Initialize the Armada engine and establish a connection to Tiller.
:param List[dict] file: Armada documents. :param List[dict] documents: Armada documents.
:param bool disable_update_pre: Disable pre-update Tiller operations. :param bool disable_update_pre: Disable pre-update Tiller operations.
:param bool disable_update_post: Disable post-update Tiller :param bool disable_update_post: Disable post-update Tiller
operations. operations.
@ -90,9 +90,9 @@ class Armada(object):
tiller_host=tiller_host, tiller_port=tiller_port, tiller_host=tiller_host, tiller_port=tiller_port,
tiller_namespace=tiller_namespace) tiller_namespace=tiller_namespace)
self.values = values self.values = values
self.documents = file self.documents = documents
self.target_manifest = target_manifest self.target_manifest = target_manifest
self.config = self.get_armada_manifest() self.manifest = self.get_armada_manifest()
def get_armada_manifest(self): def get_armada_manifest(self):
return Manifest( return Manifest(
@ -109,16 +109,26 @@ class Armada(object):
return chart, values return chart, values
def pre_flight_ops(self): def pre_flight_ops(self):
''' """Perform a series of checks and operations to ensure proper
Perform a series of checks and operations to ensure proper deployment deployment.
''' """
LOG.info("Performing pre-flight operations.")
# Ensure tiller is available and manifest is valid # Ensure Tiller is available and manifest is valid
if not self.tiller.tiller_status(): if not self.tiller.tiller_status():
raise tiller_exceptions.TillerServicesUnavailableException() raise tiller_exceptions.TillerServicesUnavailableException()
if not lint.validate_armada_documents(self.documents): valid, details = validate.validate_armada_documents(self.documents)
raise lint_exceptions.InvalidManifestException()
if details:
for msg in details:
if msg.get('error', False):
LOG.error(msg.get('message', 'Unknown validation error.'))
else:
LOG.debug(msg.get('message', 'Validation succeeded.'))
if not valid:
raise validate_exceptions.InvalidManifestException(
error_messages=details)
# Override manifest values if --set flag is used # Override manifest values if --set flag is used
if self.overrides or self.values: if self.overrides or self.values:
@ -126,20 +136,21 @@ class Armada(object):
self.documents, overrides=self.overrides, self.documents, overrides=self.overrides,
values=self.values).update_manifests() values=self.values).update_manifests()
if not lint.validate_armada_object(self.config): result, msg_list = validate.validate_armada_manifests(self.documents)
raise lint_exceptions.InvalidArmadaObjectException() if not result:
raise validate_exceptions.InvalidArmadaObjectException(
details=','.join([m.get('message') for m in msg_list]))
# Purge known releases that have failed and are in the current yaml # Purge known releases that have failed and are in the current yaml
prefix = self.config.get(const.KEYWORD_ARMADA).get( prefix = self.manifest.get(const.KEYWORD_ARMADA).get(
const.KEYWORD_PREFIX) const.KEYWORD_PREFIX)
failed_releases = self.get_releases_by_status(const.STATUS_FAILED) failed_releases = self.get_releases_by_status(const.STATUS_FAILED)
for release in failed_releases: for release in failed_releases:
for group in self.config.get(const.KEYWORD_ARMADA).get( for group in self.manifest.get(const.KEYWORD_ARMADA).get(
const.KEYWORD_GROUPS): const.KEYWORD_GROUPS):
for ch in group.get(const.KEYWORD_CHARTS): for ch in group.get(const.KEYWORD_CHARTS):
ch_release_name = release_prefix(prefix, ch_release_name = release_prefix(
ch.get('chart') prefix, ch.get('chart').get('chart_name'))
.get('chart_name'))
if release[0] == ch_release_name: if release[0] == ch_release_name:
LOG.info('Purging failed release %s ' LOG.info('Purging failed release %s '
'before deployment', release[0]) 'before deployment', release[0])
@ -150,7 +161,7 @@ class Armada(object):
# We only support a git source type right now, which can also # We only support a git source type right now, which can also
# handle git:// local paths as well # handle git:// local paths as well
repos = {} repos = {}
for group in self.config.get(const.KEYWORD_ARMADA).get( for group in self.manifest.get(const.KEYWORD_ARMADA).get(
const.KEYWORD_GROUPS): const.KEYWORD_GROUPS):
for ch in group.get(const.KEYWORD_CHARTS): for ch in group.get(const.KEYWORD_CHARTS):
self.tag_cloned_repo(ch, repos) self.tag_cloned_repo(ch, repos)
@ -229,12 +240,11 @@ class Armada(object):
# TODO: (gardlt) we need to break up this func into # TODO: (gardlt) we need to break up this func into
# a more cleaner format # a more cleaner format
LOG.info("Performing Pre-Flight Operations")
self.pre_flight_ops() self.pre_flight_ops()
# extract known charts on tiller right now # extract known charts on tiller right now
known_releases = self.tiller.list_charts() known_releases = self.tiller.list_charts()
prefix = self.config.get(const.KEYWORD_ARMADA).get( prefix = self.manifest.get(const.KEYWORD_ARMADA).get(
const.KEYWORD_PREFIX) const.KEYWORD_PREFIX)
if known_releases is None: if known_releases is None:
@ -244,7 +254,7 @@ class Armada(object):
LOG.debug("Release %s, Version %s found on Tiller", release[0], LOG.debug("Release %s, Version %s found on Tiller", release[0],
release[1]) release[1])
for entry in self.config[const.KEYWORD_ARMADA][const.KEYWORD_GROUPS]: for entry in self.manifest[const.KEYWORD_ARMADA][const.KEYWORD_GROUPS]:
chart_wait = self.wait chart_wait = self.wait
desc = entry.get('description', 'A Chart Group') desc = entry.get('description', 'A Chart Group')
@ -394,7 +404,7 @@ class Armada(object):
if self.enable_chart_cleanup: if self.enable_chart_cleanup:
self.tiller.chart_cleanup( self.tiller.chart_cleanup(
prefix, prefix,
self.config[const.KEYWORD_ARMADA][const.KEYWORD_GROUPS]) self.manifest[const.KEYWORD_ARMADA][const.KEYWORD_GROUPS])
return msg return msg
@ -403,7 +413,7 @@ class Armada(object):
Operations to run after deployment process has terminated Operations to run after deployment process has terminated
''' '''
# Delete temp dirs used for deployment # Delete temp dirs used for deployment
for group in self.config.get(const.KEYWORD_ARMADA).get( for group in self.manifest.get(const.KEYWORD_ARMADA).get(
const.KEYWORD_GROUPS): const.KEYWORD_GROUPS):
for ch in group.get(const.KEYWORD_CHARTS): for ch in group.get(const.KEYWORD_CHARTS):
if ch.get('chart').get('source').get('type') == 'git': if ch.get('chart').get('source').get('type') == 'git':

View File

@ -11,6 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from copy import deepcopy
from oslo_log import log as logging from oslo_log import log as logging
@ -47,8 +48,7 @@ class Manifest(object):
are not found or if the document types are missing required are not found or if the document types are missing required
properties. properties.
""" """
self.config = None self.documents = deepcopy(documents)
self.documents = documents
self.charts, self.groups, manifests = self._find_documents( self.charts, self.groups, manifests = self._find_documents(
target_manifest) target_manifest)
@ -66,9 +66,8 @@ class Manifest(object):
error = ('Documents must be a list of documents with at least one ' error = ('Documents must be a list of documents with at least one '
'of each of the following schemas: %s and only one ' 'of each of the following schemas: %s and only one '
'manifest' % expected_schemas) 'manifest' % expected_schemas)
LOG.error(error, expected_schemas) LOG.error(error)
raise exceptions.ManifestException( raise exceptions.ManifestException(details=error)
details=error % expected_schemas)
def _find_documents(self, target_manifest=None): def _find_documents(self, target_manifest=None):
"""Returns the chart documents, chart group documents, """Returns the chart documents, chart group documents,

View File

@ -18,7 +18,7 @@ import yaml
from armada import const from armada import const
from armada.exceptions import override_exceptions from armada.exceptions import override_exceptions
from armada.utils import lint from armada.utils import validate
class Override(object): class Override(object):
@ -152,7 +152,7 @@ class Override(object):
self.override_manifest_value(doc_path, data_path, new_value) self.override_manifest_value(doc_path, data_path, new_value)
try: try:
lint.validate_armada_documents(self.documents) validate.validate_armada_documents(self.documents)
except Exception: except Exception:
raise override_exceptions.InvalidOverrideValueException( raise override_exceptions.InvalidOverrideValueException(
self.overrides) self.overrides)

View File

@ -0,0 +1,120 @@
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# JSON schema for validating Armada charts.
---
schema: deckhand/DataSchema/v1
metadata:
name: armada/Chart/v1
schema: metadata/Control/v1
data:
$schema: http://json-schema.org/schema#
definitions:
labels:
type: object
additionalProperties:
type: string
hook_action:
type: array
items:
properties:
name:
type: string
type:
type: string
labels:
$ref: '#/definitions/labels'
required:
- type
additionalProperties: false
type: object
properties:
release:
type: string
chart_name:
type: string
namespace:
type: string
values:
type: object
dependencies:
type: array
items:
type: string
test:
type: boolean
timeout:
type: integer
wait:
type: object
properties:
timeout:
type: integer
labels:
$ref: "#/definitions/labels"
additionalProperties: false
source:
type: object
properties:
type:
type: string
location:
type: string
subpath:
type: string
reference:
type:
- string
- "null"
required:
- location
- subpath
- type
install:
# NOTE(sh8121att) Not clear that this key is actually used
# in the code. Will leave it here for backward compatabilities
# until an additional audit is done.
type: object
upgrade:
type: object
properties:
no_hooks:
type: boolean
pre:
type: object
additionalProperties: false
properties:
delete:
$ref: '#/definitions/hook_action'
update:
$ref: '#/definitions/hook_action'
create:
$ref: '#/definitions/hook_action'
post:
type: object
additionalProperties: false
properties:
create:
$ref: '#/definitions/hook_action'
required:
- no_hooks
additionalProperties: false
required:
- dependencies
- namespace
- chart_name
- release
- source
additionalProperties: false
...

View File

@ -0,0 +1,39 @@
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# JSON schema for validating Armada chart groups.
---
schema: deckhand/DataSchema/v1
metadata:
name: armada/ChartGroup/v1
schema: metadata/Control/v1
data:
$schema: http://json-schema.org/schema#
properties:
name:
type: string
description:
type: string
sequenced:
type: boolean
test_charts:
type: boolean
chart_group:
type: array
items:
type: string
required:
- chart_group
additionalProperties: false
...

View File

@ -0,0 +1,34 @@
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# JSON schema for validating Armada manifests.
---
schema: deckhand/DataSchema/v1
metadata:
name: armada/Manifest/v1
schema: metadata/Control/v1
data:
$schema: http://json-schema.org/schema#
properties:
release_prefix:
type: string
chart_groups:
type: array
items:
type: string
required:
- chart_groups
- release_prefix
additionalProperties: false
...

View File

@ -33,3 +33,55 @@ data:
release_prefix: armada release_prefix: armada
chart_groups: chart_groups:
- blog-group - blog-group
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: blog-3
data:
chart_name: blog-3
release: blog-3
namespace: default
values: {}
source:
type: git
location: https://github.com/namespace/hello-world-chart
subpath: .
reference: master
dependencies: []
---
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: blog-group3
data:
description: Deploys Simple Service
sequenced: False
chart_group:
- blog-3
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: blog-4
data:
chart_name: blog-4
release: blog-4
namespace: default
values: {}
source:
type: git
location: https://github.com/namespace/hello-world-chart
subpath: .
reference: master
dependencies: []
---
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: blog-group4
data:
description: Deploys Simple Service
sequenced: False
chart_group:
- blog-4

View File

@ -8,3 +8,55 @@ data:
chart_groups: chart_groups:
- blog-group3 - blog-group3
- blog-group4 - blog-group4
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: blog-3
data:
chart_name: blog-3
release: blog-3
namespace: default
values: {}
source:
type: git
location: https://github.com/namespace/hello-world-chart
subpath: .
reference: master
dependencies: []
---
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: blog-group3
data:
description: Deploys Simple Service
sequenced: False
chart_group:
- blog-3
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: blog-4
data:
chart_name: blog-4
release: blog-4
namespace: default
values: {}
source:
type: git
location: https://github.com/namespace/hello-world-chart
subpath: .
reference: master
dependencies: []
---
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: blog-group4
data:
description: Deploys Simple Service
sequenced: False
chart_group:
- blog-4

View File

@ -135,10 +135,10 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
} }
} }
self.assertTrue(hasattr(armada_obj, 'config')) self.assertTrue(hasattr(armada_obj, 'manifest'))
self.assertIsInstance(armada_obj.config, dict) self.assertIsInstance(armada_obj.manifest, dict)
self.assertIn('armada', armada_obj.config) self.assertIn('armada', armada_obj.manifest)
self.assertEqual(expected_config, armada_obj.config) self.assertEqual(expected_config, armada_obj.manifest)
@mock.patch.object(armada, 'source') @mock.patch.object(armada, 'source')
@mock.patch('armada.handlers.armada.Tiller') @mock.patch('armada.handlers.armada.Tiller')
@ -175,7 +175,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
armada_obj.post_flight_ops() armada_obj.post_flight_ops()
for group in armada_obj.config['armada']['chart_groups']: for group in armada_obj.manifest['armada']['chart_groups']:
for counter, chart in enumerate(group.get('chart_group')): for counter, chart in enumerate(group.get('chart_group')):
if chart.get('chart').get('source').get('type') == 'git': if chart.get('chart').get('source').get('type') == 'git':
mock_source.source_cleanup.assert_called_with( mock_source.source_cleanup.assert_called_with(
@ -193,7 +193,8 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
yaml_documents = list(yaml.safe_load_all(TEST_YAML)) yaml_documents = list(yaml.safe_load_all(TEST_YAML))
armada_obj = armada.Armada(yaml_documents) armada_obj = armada.Armada(yaml_documents)
charts = armada_obj.config['armada']['chart_groups'][0]['chart_group'] charts = armada_obj.manifest['armada']['chart_groups'][0][
'chart_group']
chart_1 = charts[0]['chart'] chart_1 = charts[0]['chart']
chart_2 = charts[1]['chart'] chart_2 = charts[1]['chart']
@ -208,7 +209,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
method_calls = [ method_calls = [
mock.call( mock.call(
mock_chartbuilder().get_helm_chart(), mock_chartbuilder().get_helm_chart(),
"{}-{}".format(armada_obj.config['armada']['release_prefix'], "{}-{}".format(armada_obj.manifest['armada']['release_prefix'],
chart_1['release']), chart_1['release']),
chart_1['namespace'], chart_1['namespace'],
dry_run=armada_obj.dry_run, dry_run=armada_obj.dry_run,
@ -217,7 +218,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
timeout=armada_obj.timeout), timeout=armada_obj.timeout),
mock.call( mock.call(
mock_chartbuilder().get_helm_chart(), mock_chartbuilder().get_helm_chart(),
"{}-{}".format(armada_obj.config['armada']['release_prefix'], "{}-{}".format(armada_obj.manifest['armada']['release_prefix'],
chart_2['release']), chart_2['release']),
chart_2['namespace'], chart_2['namespace'],
dry_run=armada_obj.dry_run, dry_run=armada_obj.dry_run,

View File

@ -21,6 +21,7 @@ import testtools
from armada import const from armada import const
from armada import exceptions from armada import exceptions
from armada.handlers import manifest from armada.handlers import manifest
from armada.utils import validate
class ManifestTestCase(testtools.TestCase): class ManifestTestCase(testtools.TestCase):
@ -411,30 +412,22 @@ class ManifestNegativeTestCase(testtools.TestCase):
"""Validate that attempting to build a chart that points to """Validate that attempting to build a chart that points to
a missing dependency fails. a missing dependency fails.
""" """
armada_manifest = manifest.Manifest(self.documents)
self.documents[1]['data']['dependencies'] = ['missing-dependency'] self.documents[1]['data']['dependencies'] = ['missing-dependency']
test_chart = armada_manifest.find_chart_document('mariadb') valid, details = validate.validate_armada_documents(self.documents)
self.assertRaises(exceptions.ManifestException, self.assertFalse(valid)
armada_manifest.build_chart_deps,
test_chart)
def test_build_chart_group_with_missing_chart_grp_fails(self): def test_build_chart_group_with_missing_chart_grp_fails(self):
"""Validate that attempting to build a chart group document with """Validate that attempting to build a chart group document with
missing chart group fails. missing chart group fails.
""" """
armada_manifest = manifest.Manifest(self.documents)
self.documents[5]['data']['chart_group'] = ['missing-chart-group'] self.documents[5]['data']['chart_group'] = ['missing-chart-group']
test_chart_group = armada_manifest.find_chart_group_document( valid, details = validate.validate_armada_documents(self.documents)
'openstack-keystone') self.assertFalse(valid)
self.assertRaises(exceptions.ManifestException,
armada_manifest.build_chart_group,
test_chart_group)
def test_build_armada_manifest_with_missing_chart_grps_fails(self): def test_build_armada_manifest_with_missing_chart_grps_fails(self):
"""Validate that attempting to build a manifest with missing """Validate that attempting to build a manifest with missing
chart groups fails. chart groups fails.
""" """
armada_manifest = manifest.Manifest(self.documents)
self.documents[6]['data']['chart_groups'] = ['missing-chart-groups'] self.documents[6]['data']['chart_groups'] = ['missing-chart-groups']
self.assertRaises(exceptions.ManifestException, valid, details = validate.validate_armada_documents(self.documents)
armada_manifest.build_armada_manifest) self.assertFalse(valid)

View File

@ -75,8 +75,12 @@ class OverrideTestCase(testtools.TestCase):
self.assertNotEqual(original_documents, documents_copy) self.assertNotEqual(original_documents, documents_copy)
# since overrides done, these documents aren't same anymore # since overrides done, these documents aren't same anymore
self.assertNotEqual(original_documents, values_documents) self.assertNotEqual(original_documents, values_documents)
target_doc = [x
for x
in ovr.documents
if x.get('metadata').get('name') == 'simple-armada'][0]
self.assertEqual('overridden', self.assertEqual('overridden',
ovr.documents[-1]['data']['release_prefix']) target_doc['data']['release_prefix'])
override = ('manifest:simple-armada:chart_groups=' override = ('manifest:simple-armada:chart_groups='
'blog-group3,blog-group4',) 'blog-group3,blog-group4',)
@ -283,8 +287,12 @@ class OverrideTestCase(testtools.TestCase):
ovr = Override(documents, override) ovr = Override(documents, override)
ovr.update_manifests() ovr.update_manifests()
ovr_doc = ovr.find_manifest_document(doc_path) ovr_doc = ovr.find_manifest_document(doc_path)
expect_doc = list(yaml.load_all(e.read()))[0] target_docs = list(yaml.load_all(e.read()))
self.assertEqual(expect_doc, ovr_doc) expected_doc = [x
for x
in target_docs
if x.get('schema') == 'armada/Manifest/v1'][0]
self.assertEqual(expected_doc.get('data'), ovr_doc.get('data'))
def test_find_manifest_document_valid(self): def test_find_manifest_document_valid(self):
expected = "{}/templates/override-{}-expected.yaml".format( expected = "{}/templates/override-{}-expected.yaml".format(

View File

@ -0,0 +1,139 @@
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: helm-toolkit
data:
chart_name: helm-toolkit
release: helm-toolkit
namespace: helm-tookit
install:
no_hooks: false
upgrade:
no_hooks: false
values: {}
source:
type: git
location: https://git.openstack.org/openstack/openstack-helm
subpath: helm-toolkit
reference: master
dependencies: []
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: mariadb
data:
chart_name: mariadb
release: mariadb
namespace: openstack
timeout: 3600
wait:
timeout: 3600
labels:
release_group: armada-mariadb
install:
no_hooks: false
upgrade:
no_hooks: false
values: {}
source:
type: git
location: https://git.openstack.org/openstack/openstack-helm
subpath: mariadb
reference: master
dependencies:
- helm-toolkit
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: memcached
data:
chart_name: memcached
release: memcached
namespace: openstack
timeout: 100
wait:
timeout: 100
labels:
release_group: armada-memcached
install:
no_hooks: false
upgrade:
no_hooks: false
values: {}
source:
type: git
location: https://git.openstack.org/openstack/openstack-helm
subpath: memcached
reference: master
dependencies:
- helm-toolkit
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: keystone
data:
chart_name: keystone
test: true
release: keystone
namespace: openstack
timeout: 100
wait:
timeout: 100
labels:
release_group: armada-keystone
install:
no_hooks: false
upgrade:
no_hooks: false
pre:
delete:
- name: keystone-bootstrap
type: job
labels:
application: keystone
component: bootstrap
values:
replicas: 3
source:
type: git
location: https://git.openstack.org/openstack/openstack-helm
subpath: keystone
reference: master
dependencies:
- helm-toolkit
---
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: keystone-infra-services
data:
description: "Keystone Infra Services"
sequenced: True
chart_group:
- mariadb
- memcached
---
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: openstack-keystone
data:
description: "Deploying OpenStack Keystone"
sequenced: True
test_charts: False
chart_group:
- keystone
---
schema: armada/Manifest/v1
metadata:
schema: metadata/Document/v1
name: armada-manifest
data:
release_prefix: armada
chart_groups:
- keystone-infra-services
- openstack-keystone

View File

@ -1,56 +0,0 @@
---
schema: armada/Manifest/v1
metadata:
schema: metadata/Document/v1
name: example-manifest
data:
release_prefix: example
chart_groups:
- example-group
---
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: example-group
data:
description: "OpenStack Infra Services"
chart_group:
- example-chart
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: example-chart
data:
name: keystone
release: keystone
namespace: undercloud
timeout: 100
install:
no_hooks: false
upgrade:
no_hooks: false
values: {}
source:
type: git
location: git://github.com/example/example
subpath: example-chart
reference: master
dependencies:
- dep-chart
---
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: dep-chart
data:
name: dep-chart
release: null
namespace: null
values: {}
source:
type: git
location: git://github.com/example/example
subpath: dep-chart
reference: master
dependencies: []

View File

@ -1,168 +0,0 @@
# Copyright 2017 The Armada Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
import yaml
import os
from armada.utils import lint
class LintTestCase(unittest.TestCase):
def setUp(self):
self.basepath = os.path.join(os.path.dirname(__file__))
def test_lint_armada_yaml_pass(self):
template = '{}/templates/valid_armada_document.yaml'.format(
self.basepath)
document = yaml.safe_load_all(open(template).read())
resp = lint.validate_armada_documents(document)
self.assertTrue(resp)
def test_lint_armada_manifest_no_groups(self):
template_manifest = """
schema: armada/Manifest/v1
metadata:
schema: metadata/Document/v1
name: example-manifest
data:
release_prefix: example
"""
document = yaml.safe_load_all(template_manifest)
with self.assertRaises(Exception):
lint.validate_armada_documents(document)
def test_lint_validate_manifest_pass(self):
template_manifest = """
schema: armada/Manifest/v1
metadata:
schema: metadata/Document/v1
name: example-manifest
data:
release_prefix: example
chart_groups:
- example-group
"""
document = yaml.safe_load_all(template_manifest)
self.assertTrue(lint.validate_manifest_document(document))
def test_lint_validate_manifest_no_prefix(self):
template_manifest = """
schema: armada/Manifest/v1
metadata:
schema: metadata/Document/v1
name: example-manifest
data:
chart_groups:
- example-group
"""
document = yaml.safe_load_all(template_manifest)
with self.assertRaises(Exception):
lint.validate_manifest_document(document)
def test_lint_validate_group_pass(self):
template_manifest = """
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: example-manifest
data:
description: this is sample
chart_group:
- example-group
"""
document = yaml.safe_load_all(template_manifest)
self.assertTrue(lint.validate_chart_group_document(document))
def test_lint_validate_group_no_chart_group(self):
template_manifest = """
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: example-manifest
data:
description: this is sample
"""
document = yaml.safe_load_all(template_manifest)
with self.assertRaises(Exception):
lint.validate_chart_group_document(document)
def test_lint_validate_chart_pass(self):
template_manifest = """
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: example-chart
data:
name: keystone
release: keystone
namespace: undercloud
timeout: 100
install:
no_hooks: false
upgrade:
no_hooks: false
values: {}
source:
type: git
location: git://github.com/example/example
subpath: example-chart
reference: master
dependencies:
- dep-chart
"""
document = yaml.safe_load_all(template_manifest)
self.assertTrue(lint.validate_chart_document(document))
def test_lint_validate_chart_no_release(self):
template_manifest = """
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: example-chart
data:
name: keystone
namespace: undercloud
timeout: 100
install:
no_hooks: false
upgrade:
no_hooks: false
values: {}
source:
type: git
location: git://github.com/example/example
subpath: example-chart
reference: master
dependencies:
- dep-chart
"""
document = yaml.safe_load_all(template_manifest)
with self.assertRaises(Exception):
lint.validate_chart_document(document)
def test_lint_validate_manifest_url(self):
value = 'url'
assert lint.validate_manifest_url(value) is False
value = 'https://raw.githubusercontent.com/att-comdev/' \
'armada/master/examples/simple.yaml'
assert lint.validate_manifest_url(value) is True
def test_lint_validate_manifest_filepath(self):
value = 'filepath'
assert lint.validate_manifest_filepath(value) is False
value = '{}/templates/valid_armada_document.yaml'.format(
self.basepath)
assert lint.validate_manifest_filepath(value) is True

View File

@ -0,0 +1,302 @@
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import yaml
from armada.tests.unit import base
from armada.utils import validate
template_chart = """
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: example-chart
data:
chart_name: keystone
release: keystone
namespace: undercloud
timeout: 100
install:
no_hooks: false
upgrade:
no_hooks: false
values: {}
source:
type: git
location: git://github.com/example/example
subpath: example-chart
reference: master
dependencies: []
"""
template_chart_group = """
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: example-manifest
data:
description: this is sample
chart_group:
- example-chart
"""
template_manifest = """
schema: armada/Manifest/v1
metadata:
schema: metadata/Document/v1
name: example-manifest
data:
release_prefix: example
chart_groups:
- example-chart
"""
class BaseValidateTest(base.ArmadaTestCase):
def setUp(self):
super(BaseValidateTest, self).setUp()
self.basepath = os.path.join(os.path.dirname(__file__), os.pardir)
def _build_error_message(self, document, name, message):
return "Invalid document [{}] {}: {}.".format(document, name, message)
class ValidateTestCase(BaseValidateTest):
def test_validate_load_schemas(self):
expected_schemas = [
'armada/Chart/v1',
'armada/ChartGroup/v1',
'armada/Manifest/v1'
]
for expected_schema in expected_schemas:
self.assertIn(expected_schema, validate.SCHEMAS)
def test_validate_armada_yaml_passes(self):
template = '{}/resources/valid_armada_document.yaml'.format(
self.basepath)
with open(template) as f:
documents = yaml.safe_load_all(f.read())
valid, details = validate.validate_armada_documents(list(documents))
self.assertTrue(valid)
def test_validate_manifest_passes(self):
manifest = yaml.safe_load(template_manifest)
is_valid, error = validate.validate_armada_document(manifest)
self.assertTrue(is_valid)
def test_validate_chart_group_with_values(self):
test_chart_group = """
---
schema: armada/ChartGroup/v1
metadata:
name: kubernetes-proxy
schema: metadata/Document/v1
data:
description: Kubernetes proxy
name: kubernetes-proxy
sequenced: true
chart_group:
- proxy
---
schema: armada/Chart/v1
metadata:
name: proxy
schema: metadata/Document/v1
data:
chart_name: proxy
timeout: 600
release: kubernetes-proxy
source:
subpath: proxy
type: local
location: "/etc/genesis/armada/assets/charts"
namespace: kube-system
upgrade:
no_hooks: true
values:
images:
tags:
proxy: gcr.io/google_containers/hyperkube-amd64:v1.8.6
network:
kubernetes_netloc: 127.0.0.1:6553
dependencies:
- helm-toolkit
---
schema: armada/Chart/v1
metadata:
name: helm-toolkit
schema: metadata/Document/v1
data:
chart_name: helm-toolkit
wait:
timeout: 600
release: helm-toolkit
source:
reference: master
subpath: helm-toolkit
location: https://git.openstack.org/openstack/openstack-helm
type: git
namespace: helm-toolkit
upgrade:
no_hooks: true
values: {}
dependencies: []
"""
chart_group = yaml.safe_load_all(test_chart_group)
is_valid, error = validate.validate_armada_documents(list(chart_group))
self.assertTrue(is_valid)
def test_validate_group_passes(self):
chart_group = yaml.safe_load(template_chart_group)
is_valid, error = validate.validate_armada_document(chart_group)
self.assertTrue(is_valid)
def test_validate_chart_passes(self):
chart = yaml.safe_load(template_chart)
is_valid, error = validate.validate_armada_document(chart)
self.assertTrue(is_valid)
def test_validate_manifest_url(self):
value = 'url'
self.assertFalse(validate.validate_manifest_url(value))
value = 'https://raw.githubusercontent.com/att-comdev/' \
'armada/master/examples/simple.yaml'
self.assertTrue(validate.validate_manifest_url(value))
def test_validate_manifest_filepath(self):
value = 'filepath'
self.assertFalse(validate.validate_manifest_filepath(value))
value = '{}/resources/valid_armada_document.yaml'.format(
self.basepath)
self.assertTrue(validate.validate_manifest_filepath(value))
class ValidateNegativeTestCase(BaseValidateTest):
def test_validate_load_duplicate_schemas_expect_runtime_error(self):
"""Validate that calling ``validate._load_schemas`` results in a
``RuntimeError`` being thrown, because the call is made during module
import, and importing the schemas again in manually results in
duplicates.
"""
with self.assertRaisesRegexp(
RuntimeError,
'Duplicate schema specified for: .*'):
validate._load_schemas()
def test_validate_no_dictionary_expect_type_error(self):
expected_error = 'The provided input "invalid" must be a dictionary.'
self.assertRaisesRegexp(TypeError, expected_error,
validate.validate_armada_documents,
['invalid'])
def test_validate_invalid_chart_armada_manifest(self):
template = '{}/resources/valid_armada_document.yaml'.format(
self.basepath)
with open(template) as f:
documents = list(yaml.safe_load_all(f.read()))
mariadb_document = [
d for d in documents if d['metadata']['name'] == 'mariadb'][0]
del mariadb_document['data']['release']
_, error_messages = validate.validate_armada_documents(documents)
expected_error = self._build_error_message(
'armada/Chart/v1', 'mariadb',
"'release' is a required property")
self.assertEqual(1, len(error_messages))
self.assertEqual(expected_error, error_messages[0]['message'])
def test_validate_validate_group_without_required_chart_group(self):
template_manifest = """
schema: armada/ChartGroup/v1
metadata:
schema: metadata/Document/v1
name: example-manifest
data:
description: this is sample
"""
document = yaml.safe_load(template_manifest)
is_valid, error = validate.validate_armada_document(document)
expected_error = self._build_error_message(
'armada/ChartGroup/v1', 'example-manifest',
"'chart_group' is a required property")
self.assertFalse(is_valid)
self.assertEqual(error[0]['message'], expected_error)
def test_validate_manifest_without_required_release_prefix(self):
template_manifest = """
schema: armada/Manifest/v1
metadata:
schema: metadata/Document/v1
name: example-manifest
data:
chart_groups:
- example-group
"""
document = yaml.safe_load(template_manifest)
is_valid, error = validate.validate_armada_document(document)
expected_error = self._build_error_message(
'armada/Manifest/v1', 'example-manifest',
"'release_prefix' is a required property")
self.assertFalse(is_valid)
self.assertEqual(error[0]['message'], expected_error)
def test_validate_chart_without_required_release_property(self):
template_manifest = """
schema: armada/Chart/v1
metadata:
schema: metadata/Document/v1
name: example-chart
data:
chart_name: keystone
namespace: undercloud
timeout: 100
install:
no_hooks: false
upgrade:
no_hooks: false
values: {}
source:
type: git
location: git://github.com/example/example
subpath: example-chart
reference: master
dependencies:
- dep-chart
"""
document = yaml.safe_load(template_manifest)
is_valid, error = validate.validate_armada_document(document)
expected_error = self._build_error_message(
'armada/Chart/v1', 'example-chart',
"'release' is a required property")
self.assertFalse(is_valid)
self.assertEqual(error[0]['message'], expected_error)

View File

@ -1,109 +0,0 @@
# Copyright 2017 The Armada Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import requests
from armada.const import DOCUMENT_CHART, DOCUMENT_GROUP, DOCUMENT_MANIFEST
from armada.const import KEYWORD_ARMADA, KEYWORD_PREFIX, KEYWORD_GROUPS, \
KEYWORD_CHARTS, KEYWORD_RELEASE
def validate_armada_documents(documents):
manifest = validate_manifest_document(documents)
group = validate_chart_group_document(documents)
chart = validate_chart_document(documents)
return manifest and group and chart
def validate_armada_object(object):
if not isinstance(object.get(KEYWORD_ARMADA, None), dict):
raise Exception("Could not find {} keyword".format(KEYWORD_ARMADA))
armada_object = object.get('armada')
if armada_object.get(KEYWORD_PREFIX, None) is None:
raise Exception("Could not find {} keyword".format(KEYWORD_PREFIX))
if not isinstance(armada_object.get(KEYWORD_GROUPS), list):
raise Exception('{} is of correct type: {} (expected: {} )'.format(
KEYWORD_GROUPS, type(armada_object.get(KEYWORD_GROUPS)), list))
for group in armada_object.get(KEYWORD_GROUPS):
for chart in group.get(KEYWORD_CHARTS):
chart_obj = chart.get('chart')
if chart_obj.get(KEYWORD_RELEASE, None) is None:
raise Exception('Could not find {} in {}'.format(
KEYWORD_RELEASE, chart_obj.get('release')))
return True
def validate_manifest_document(documents):
manifest_documents = []
for document in documents:
if document.get('schema') == DOCUMENT_MANIFEST:
manifest_documents.append(document)
manifest_data = document.get('data')
if not manifest_data.get(KEYWORD_PREFIX, False):
raise Exception(
'Missing {} keyword in manifest'.format(KEYWORD_PREFIX))
if not isinstance(manifest_data.get('chart_groups'),
list) and not manifest_data.get(
'chart_groups', False):
raise Exception('Missing %s values. Expecting list type'.
format(KEYWORD_GROUPS))
if len(manifest_documents) > 1:
raise Exception(
'Schema {} must be unique'.format(DOCUMENT_MANIFEST))
return True
def validate_chart_group_document(documents):
for document in documents:
if document.get('schema') == DOCUMENT_GROUP:
manifest_data = document.get('data')
if not isinstance(manifest_data.get(KEYWORD_CHARTS),
list) and not manifest_data.get(
'chart_group', False):
raise Exception('Missing %s values. Expecting a list type'.
format(KEYWORD_CHARTS))
return True
def validate_chart_document(documents):
for document in documents:
if document.get('schema') == DOCUMENT_CHART:
manifest_data = document.get('data')
if not manifest_data.get(KEYWORD_RELEASE, False):
raise Exception(
'Missing %s values in %s. Expecting a string type'.format(
KEYWORD_RELEASE, document.get('metadata').get('name')))
return True
def validate_manifest_url(value):
try:
return (requests.get(value).status_code == 200)
except:
return False
def validate_manifest_filepath(value):
return os.path.isfile(value)

210
armada/utils/validate.py Normal file
View File

@ -0,0 +1,210 @@
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import jsonschema
import os
import pkg_resources
import requests
import yaml
from oslo_log import log as logging
from armada.const import KEYWORD_GROUPS, KEYWORD_CHARTS, KEYWORD_RELEASE
from armada.handlers.manifest import Manifest
from armada.exceptions.manifest_exceptions import ManifestException
LOG = logging.getLogger(__name__)
# Creates a mapping between ``metadata.name``: ``data`` where the
# ``metadata.name`` is the ``schema`` of a manifest and the ``data`` is the
# JSON schema to be used to validate the manifest in question.
SCHEMAS = {}
def _get_schema_dir():
return pkg_resources.resource_filename('armada', 'schemas')
def _load_schemas():
"""Populates ``SCHEMAS`` with the schemas defined in package
``armada.schemas``.
"""
schema_dir = _get_schema_dir()
for schema_file in os.listdir(schema_dir):
with open(os.path.join(schema_dir, schema_file)) as f:
for schema in yaml.safe_load_all(f):
name = schema['metadata']['name']
if name in SCHEMAS:
raise RuntimeError(
'Duplicate schema specified for: %s.' % name)
SCHEMAS[name] = schema['data']
def _validate_armada_manifest(manifest):
"""Validates an Armada manifest file output by
:class:`armada.handlers.manifest.Manifest`.
This will do business logic validation after the input
files have be syntatically validated via jsonschema.
:param dict manifest: The manifest to validate.
:returns: A tuple of (bool, list[dict]) where the first value
indicates whether the validation succeeded or failed and
the second value is the validation details with a minimum
keyset of (message(str), error(bool))
:rtype: tuple.
"""
details = []
try:
armada_object = manifest.get_manifest().get('armada')
except ManifestException as me:
details.append(dict(message=str(me), error=True))
return False, details
groups = armada_object.get(KEYWORD_GROUPS)
if not isinstance(groups, list):
message = '{} entry is of wrong type: {} (expected: {})'.format(
KEYWORD_GROUPS, type(groups), 'list')
details.append(dict(message=message, error=True))
for group in groups:
for chart in group.get(KEYWORD_CHARTS):
chart_obj = chart.get('chart')
if KEYWORD_RELEASE not in chart_obj:
message = 'Could not find {} keyword in {}'.format(
KEYWORD_RELEASE, chart_obj.get('release'))
details.append(dict(message=message, error=True))
if len([x for x in details if x.get('error', False)]) > 0:
return False, details
return True, details
def validate_armada_manifests(documents):
"""Validate each Aramada manifest found in the document set.
:param documents: List of Armada documents to validate
:type documents: :func: `list[dict]`.
"""
messages = []
all_valid = True
for document in documents:
if document.get('schema', '') == 'armada/Manifest/v1':
target = document.get('metadata').get('name')
manifest = Manifest(documents,
target_manifest=target)
is_valid, details = _validate_armada_manifest(manifest)
all_valid = all_valid and is_valid
messages.extend(details)
return all_valid, messages
def validate_armada_document(document):
"""Validates a document ingested by Armada by subjecting it to JSON schema
validation.
:param dict dictionary: The document to validate.
:returns: A tuple of (bool, list[dict]) where the first value
indicates whether the validation succeeded or failed and
the second value is the validation details with a minimum
keyset of (message(str), error(bool))
:rtype: tuple.
:raises TypeError: If ``document`` is not of type ``dict``.
"""
if not isinstance(document, dict):
raise TypeError('The provided input "%s" must be a dictionary.'
% document)
schema = document.get('schema', '<missing>')
document_name = document.get('metadata', {}).get('name', None)
details = []
if schema in SCHEMAS:
try:
validator = jsonschema.Draft4Validator(SCHEMAS[schema])
for error in validator.iter_errors(document.get('data')):
msg = "Invalid document [%s] %s: %s." % \
(schema, document_name, error.message)
details.append(dict(message=msg,
error=True,
doc_schema=schema,
doc_name=document_name))
except jsonschema.SchemaError as e:
error_message = ('The built-in Armada JSON schema %s is invalid. '
'Details: %s.' % (e.schema, e.message))
LOG.error(error_message)
details.append(dict(message=error_message, error=True))
else:
error_message = (
'Document [%s] %s is not supported.' %
(schema, document_name))
LOG.info(error_message)
details.append(dict(message=error_message, error=False))
if len([x for x in details if x.get('error', False)]) > 0:
return False, details
return True, details
def validate_armada_documents(documents):
"""Validates multiple Armada documents.
:param documents: List of Armada maanifests to validate.
:type documents: :func:`list[dict]`.
:returns: A tuple of bool, list[dict] where the first value is whether
the full set of documents is valid or not and the second is the
detail messages from validation
:rtype: tuple
"""
messages = []
# Track if all the documents in the set are valid
all_valid = True
for document in documents:
is_valid, details = validate_armada_document(document)
all_valid = all_valid and is_valid
messages.extend(details)
if all_valid:
valid, details = validate_armada_manifests(documents)
all_valid = all_valid and valid
messages.extend(details)
return all_valid, messages
def validate_manifest_url(value):
try:
return (requests.get(value).status_code == 200)
except:
return False
def validate_manifest_filepath(value):
return os.path.isfile(value)
# Fill the cache.
_load_schemas()

View File

@ -7,6 +7,10 @@ data:
chart_name: helm-toolkit chart_name: helm-toolkit
release: helm-toolkit release: helm-toolkit
namespace: helm-tookit namespace: helm-tookit
install:
no_hooks: false
upgrade:
no_hooks: false
values: {} values: {}
source: source:
type: git type: git

View File

@ -10,6 +10,7 @@ requests
supermutes==0.2.5 supermutes==0.2.5
Paste>=2.0.3 Paste>=2.0.3
PasteDeploy>=1.5.2 PasteDeploy>=1.5.2
jsonschema>=2.6.0
# API # API
falcon falcon