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.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
@ -50,7 +50,7 @@ class Validate(api.BaseResource):
self.logger.debug("Validating set of %d documents."
% len(documents))
result = validate_armada_documents(documents)
result, details = validate_armada_documents(documents)
resp.content_type = 'application/json'
resp_body = {
@ -58,12 +58,14 @@ class Validate(api.BaseResource):
'apiVersion': 'v1.0',
'metadata': {},
'reason': 'Validation',
'details': {
'errorCount': 0,
'messageList': []
},
'details': {},
}
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:
resp.status = falcon.HTTP_200
resp_body['status'] = 'Success'
@ -74,9 +76,6 @@ class Validate(api.BaseResource):
resp_body['status'] = 'Failure'
resp_body['message'] = 'Armada validations failed'
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)
except Exception as ex:

View File

@ -16,9 +16,7 @@ import click
import yaml
from armada.cli import CliAction
from armada.utils.lint import validate_armada_documents
from armada.utils.lint import validate_armada_object
from armada.handlers.manifest import Manifest
from armada.utils.validate import validate_armada_documents
from armada.handlers.document import ReferenceResolver
@ -64,16 +62,20 @@ class ValidateManifest(CliAction):
for d in doc_data:
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:
if doc_check and obj_check:
valid, details = validate_armada_documents(documents)
if valid:
self.logger.info('Successfully validated: %s',
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:
raise Exception('Failed to validate: %s', self.locations)
raise Exception('Exception raised during '
'validation: %s', self.locations)
else:
if len(self.locations) > 1:
self.logger.error(
@ -88,4 +90,7 @@ class ValidateManifest(CliAction):
if resp.get('code') == 200:
self.logger.info('Successfully validated: %s', self.locations)
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
class LintException(base_exception.ArmadaBaseException):
class ValidateException(base_exception.ArmadaBaseException):
'''Base class for linting exceptions and errors.'''
message = 'An unknown linting error occurred.'
class InvalidManifestException(LintException):
class InvalidManifestException(ValidateException):
'''
Exception for invalid manifests.
@ -29,28 +29,29 @@ class InvalidManifestException(LintException):
*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.'''
message = 'Chart name must be a string.'
class InvalidChartDefinitionException(LintException):
class InvalidChartDefinitionException(ValidateException):
'''Exception when invalid chart definition is encountered.'''
message = 'Invalid chart definition. Chart definition must be array.'
class InvalidReleaseException(LintException):
class InvalidReleaseException(ValidateException):
'''Exception that occurs when a release is invalid.'''
message = 'Release needs to be a string.'
class InvalidArmadaObjectException(LintException):
class InvalidArmadaObjectException(ValidateException):
'''
Exception that occurs when an Armada object is not declared.
@ -58,4 +59,5 @@ class InvalidArmadaObjectException(LintException):
*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.exceptions import armada_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.utils.release import release_prefix
from armada.utils import source
from armada.utils import lint
from armada.utils import validate
from armada import const
LOG = logging.getLogger(__name__)
@ -44,7 +44,7 @@ class Armada(object):
'''
def __init__(self,
file,
documents,
disable_update_pre=False,
disable_update_post=False,
enable_chart_cleanup=False,
@ -60,7 +60,7 @@ class Armada(object):
'''
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_post: Disable post-update Tiller
operations.
@ -90,9 +90,9 @@ class Armada(object):
tiller_host=tiller_host, tiller_port=tiller_port,
tiller_namespace=tiller_namespace)
self.values = values
self.documents = file
self.documents = documents
self.target_manifest = target_manifest
self.config = self.get_armada_manifest()
self.manifest = self.get_armada_manifest()
def get_armada_manifest(self):
return Manifest(
@ -109,16 +109,26 @@ class Armada(object):
return chart, values
def pre_flight_ops(self):
'''
Perform a series of checks and operations to ensure proper deployment
'''
"""Perform a series of checks and operations to ensure proper
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():
raise tiller_exceptions.TillerServicesUnavailableException()
if not lint.validate_armada_documents(self.documents):
raise lint_exceptions.InvalidManifestException()
valid, details = validate.validate_armada_documents(self.documents)
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
if self.overrides or self.values:
@ -126,20 +136,21 @@ class Armada(object):
self.documents, overrides=self.overrides,
values=self.values).update_manifests()
if not lint.validate_armada_object(self.config):
raise lint_exceptions.InvalidArmadaObjectException()
result, msg_list = validate.validate_armada_manifests(self.documents)
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
prefix = self.config.get(const.KEYWORD_ARMADA).get(
prefix = self.manifest.get(const.KEYWORD_ARMADA).get(
const.KEYWORD_PREFIX)
failed_releases = self.get_releases_by_status(const.STATUS_FAILED)
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):
for ch in group.get(const.KEYWORD_CHARTS):
ch_release_name = release_prefix(prefix,
ch.get('chart')
.get('chart_name'))
ch_release_name = release_prefix(
prefix, ch.get('chart').get('chart_name'))
if release[0] == ch_release_name:
LOG.info('Purging failed release %s '
'before deployment', release[0])
@ -150,7 +161,7 @@ class Armada(object):
# We only support a git source type right now, which can also
# handle git:// local paths as well
repos = {}
for group in self.config.get(const.KEYWORD_ARMADA).get(
for group in self.manifest.get(const.KEYWORD_ARMADA).get(
const.KEYWORD_GROUPS):
for ch in group.get(const.KEYWORD_CHARTS):
self.tag_cloned_repo(ch, repos)
@ -229,12 +240,11 @@ class Armada(object):
# TODO: (gardlt) we need to break up this func into
# a more cleaner format
LOG.info("Performing Pre-Flight Operations")
self.pre_flight_ops()
# extract known charts on tiller right now
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)
if known_releases is None:
@ -244,7 +254,7 @@ class Armada(object):
LOG.debug("Release %s, Version %s found on Tiller", release[0],
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
desc = entry.get('description', 'A Chart Group')
@ -394,7 +404,7 @@ class Armada(object):
if self.enable_chart_cleanup:
self.tiller.chart_cleanup(
prefix,
self.config[const.KEYWORD_ARMADA][const.KEYWORD_GROUPS])
self.manifest[const.KEYWORD_ARMADA][const.KEYWORD_GROUPS])
return msg
@ -403,7 +413,7 @@ class Armada(object):
Operations to run after deployment process has terminated
'''
# 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):
for ch in group.get(const.KEYWORD_CHARTS):
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.
# See the License for the specific language governing permissions and
# limitations under the License.
from copy import deepcopy
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
properties.
"""
self.config = None
self.documents = documents
self.documents = deepcopy(documents)
self.charts, self.groups, manifests = self._find_documents(
target_manifest)
@ -66,9 +66,8 @@ class Manifest(object):
error = ('Documents must be a list of documents with at least one '
'of each of the following schemas: %s and only one '
'manifest' % expected_schemas)
LOG.error(error, expected_schemas)
raise exceptions.ManifestException(
details=error % expected_schemas)
LOG.error(error)
raise exceptions.ManifestException(details=error)
def _find_documents(self, target_manifest=None):
"""Returns the chart documents, chart group documents,

View File

@ -18,7 +18,7 @@ import yaml
from armada import const
from armada.exceptions import override_exceptions
from armada.utils import lint
from armada.utils import validate
class Override(object):
@ -152,7 +152,7 @@ class Override(object):
self.override_manifest_value(doc_path, data_path, new_value)
try:
lint.validate_armada_documents(self.documents)
validate.validate_armada_documents(self.documents)
except Exception:
raise override_exceptions.InvalidOverrideValueException(
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
chart_groups:
- 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:
- blog-group3
- 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.assertIsInstance(armada_obj.config, dict)
self.assertIn('armada', armada_obj.config)
self.assertEqual(expected_config, armada_obj.config)
self.assertTrue(hasattr(armada_obj, 'manifest'))
self.assertIsInstance(armada_obj.manifest, dict)
self.assertIn('armada', armada_obj.manifest)
self.assertEqual(expected_config, armada_obj.manifest)
@mock.patch.object(armada, 'source')
@mock.patch('armada.handlers.armada.Tiller')
@ -175,7 +175,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
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')):
if chart.get('chart').get('source').get('type') == 'git':
mock_source.source_cleanup.assert_called_with(
@ -193,7 +193,8 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
yaml_documents = list(yaml.safe_load_all(TEST_YAML))
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_2 = charts[1]['chart']
@ -208,7 +209,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
method_calls = [
mock.call(
mock_chartbuilder().get_helm_chart(),
"{}-{}".format(armada_obj.config['armada']['release_prefix'],
"{}-{}".format(armada_obj.manifest['armada']['release_prefix'],
chart_1['release']),
chart_1['namespace'],
dry_run=armada_obj.dry_run,
@ -217,7 +218,7 @@ class ArmadaHandlerTestCase(base.ArmadaTestCase):
timeout=armada_obj.timeout),
mock.call(
mock_chartbuilder().get_helm_chart(),
"{}-{}".format(armada_obj.config['armada']['release_prefix'],
"{}-{}".format(armada_obj.manifest['armada']['release_prefix'],
chart_2['release']),
chart_2['namespace'],
dry_run=armada_obj.dry_run,

View File

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

View File

@ -75,8 +75,12 @@ class OverrideTestCase(testtools.TestCase):
self.assertNotEqual(original_documents, documents_copy)
# since overrides done, these documents aren't same anymore
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',
ovr.documents[-1]['data']['release_prefix'])
target_doc['data']['release_prefix'])
override = ('manifest:simple-armada:chart_groups='
'blog-group3,blog-group4',)
@ -283,8 +287,12 @@ class OverrideTestCase(testtools.TestCase):
ovr = Override(documents, override)
ovr.update_manifests()
ovr_doc = ovr.find_manifest_document(doc_path)
expect_doc = list(yaml.load_all(e.read()))[0]
self.assertEqual(expect_doc, ovr_doc)
target_docs = list(yaml.load_all(e.read()))
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):
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
release: helm-toolkit
namespace: helm-tookit
install:
no_hooks: false
upgrade:
no_hooks: false
values: {}
source:
type: git

View File

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