From 3b879fc8462b4908722e622ab36adf61e5953656 Mon Sep 17 00:00:00 2001 From: gardlt Date: Fri, 15 Sep 2017 03:45:06 +0000 Subject: [PATCH] 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 --- armada/api/controller/validation.py | 17 +- armada/cli/validate.py | 25 +- ...t_exceptions.py => validate_exceptions.py} | 18 +- armada/handlers/armada.py | 60 ++-- armada/handlers/manifest.py | 9 +- armada/handlers/override.py | 4 +- armada/schemas/armada-chart-schema.yaml | 120 +++++++ armada/schemas/armada-chartgroup-schema.yaml | 39 +++ armada/schemas/armada-manifest-schema.yaml | 34 ++ .../tests/unit/handlers/templates/base.yaml | 52 +++ .../templates/override-03-expected.yaml | 52 +++ armada/tests/unit/handlers/test_armada.py | 17 +- armada/tests/unit/handlers/test_manifest.py | 21 +- armada/tests/unit/handlers/test_override.py | 14 +- .../unit/resources/valid_armada_document.yaml | 139 ++++++++ .../templates/valid_armada_document.yaml | 56 ---- armada/tests/unit/utils/test_lint.py | 168 ---------- armada/tests/unit/utils/test_validate.py | 302 ++++++++++++++++++ armada/utils/lint.py | 109 ------- armada/utils/validate.py | 210 ++++++++++++ examples/keystone-manifest.yaml | 4 + requirements.txt | 1 + 22 files changed, 1054 insertions(+), 417 deletions(-) rename armada/exceptions/{lint_exceptions.py => validate_exceptions.py} (71%) create mode 100644 armada/schemas/armada-chart-schema.yaml create mode 100644 armada/schemas/armada-chartgroup-schema.yaml create mode 100644 armada/schemas/armada-manifest-schema.yaml create mode 100644 armada/tests/unit/resources/valid_armada_document.yaml delete mode 100644 armada/tests/unit/utils/templates/valid_armada_document.yaml delete mode 100644 armada/tests/unit/utils/test_lint.py create mode 100644 armada/tests/unit/utils/test_validate.py delete mode 100644 armada/utils/lint.py create mode 100644 armada/utils/validate.py diff --git a/armada/api/controller/validation.py b/armada/api/controller/validation.py index 7b6d3395..9fccf391 100644 --- a/armada/api/controller/validation.py +++ b/armada/api/controller/validation.py @@ -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: diff --git a/armada/cli/validate.py b/armada/cli/validate.py index 0cbb9246..72170a24 100644 --- a/armada/cli/validate.py +++ b/armada/cli/validate.py @@ -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)) diff --git a/armada/exceptions/lint_exceptions.py b/armada/exceptions/validate_exceptions.py similarity index 71% rename from armada/exceptions/lint_exceptions.py rename to armada/exceptions/validate_exceptions.py index f70301af..c7cc7565 100644 --- a/armada/exceptions/lint_exceptions.py +++ b/armada/exceptions/validate_exceptions.py @@ -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.') diff --git a/armada/handlers/armada.py b/armada/handlers/armada.py index adf4039a..22e6e079 100644 --- a/armada/handlers/armada.py +++ b/armada/handlers/armada.py @@ -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': diff --git a/armada/handlers/manifest.py b/armada/handlers/manifest.py index f6597754..b9819504 100644 --- a/armada/handlers/manifest.py +++ b/armada/handlers/manifest.py @@ -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, diff --git a/armada/handlers/override.py b/armada/handlers/override.py index e4d9aa19..90a048d2 100644 --- a/armada/handlers/override.py +++ b/armada/handlers/override.py @@ -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) diff --git a/armada/schemas/armada-chart-schema.yaml b/armada/schemas/armada-chart-schema.yaml new file mode 100644 index 00000000..8b31cf36 --- /dev/null +++ b/armada/schemas/armada-chart-schema.yaml @@ -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 +... diff --git a/armada/schemas/armada-chartgroup-schema.yaml b/armada/schemas/armada-chartgroup-schema.yaml new file mode 100644 index 00000000..36b3ac46 --- /dev/null +++ b/armada/schemas/armada-chartgroup-schema.yaml @@ -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 +... diff --git a/armada/schemas/armada-manifest-schema.yaml b/armada/schemas/armada-manifest-schema.yaml new file mode 100644 index 00000000..4bdd677c --- /dev/null +++ b/armada/schemas/armada-manifest-schema.yaml @@ -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 +... diff --git a/armada/tests/unit/handlers/templates/base.yaml b/armada/tests/unit/handlers/templates/base.yaml index 8f3851d0..687aa2e7 100644 --- a/armada/tests/unit/handlers/templates/base.yaml +++ b/armada/tests/unit/handlers/templates/base.yaml @@ -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 diff --git a/armada/tests/unit/handlers/templates/override-03-expected.yaml b/armada/tests/unit/handlers/templates/override-03-expected.yaml index 7a1a2057..6c91f57e 100644 --- a/armada/tests/unit/handlers/templates/override-03-expected.yaml +++ b/armada/tests/unit/handlers/templates/override-03-expected.yaml @@ -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 diff --git a/armada/tests/unit/handlers/test_armada.py b/armada/tests/unit/handlers/test_armada.py index f7880d0b..b2d53963 100644 --- a/armada/tests/unit/handlers/test_armada.py +++ b/armada/tests/unit/handlers/test_armada.py @@ -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, diff --git a/armada/tests/unit/handlers/test_manifest.py b/armada/tests/unit/handlers/test_manifest.py index 4313b0c9..e0c9a403 100644 --- a/armada/tests/unit/handlers/test_manifest.py +++ b/armada/tests/unit/handlers/test_manifest.py @@ -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) diff --git a/armada/tests/unit/handlers/test_override.py b/armada/tests/unit/handlers/test_override.py index 5eeaf963..807c313f 100644 --- a/armada/tests/unit/handlers/test_override.py +++ b/armada/tests/unit/handlers/test_override.py @@ -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( diff --git a/armada/tests/unit/resources/valid_armada_document.yaml b/armada/tests/unit/resources/valid_armada_document.yaml new file mode 100644 index 00000000..4047ebbd --- /dev/null +++ b/armada/tests/unit/resources/valid_armada_document.yaml @@ -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 diff --git a/armada/tests/unit/utils/templates/valid_armada_document.yaml b/armada/tests/unit/utils/templates/valid_armada_document.yaml deleted file mode 100644 index 6a1fe13f..00000000 --- a/armada/tests/unit/utils/templates/valid_armada_document.yaml +++ /dev/null @@ -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: [] diff --git a/armada/tests/unit/utils/test_lint.py b/armada/tests/unit/utils/test_lint.py deleted file mode 100644 index e54424cb..00000000 --- a/armada/tests/unit/utils/test_lint.py +++ /dev/null @@ -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 diff --git a/armada/tests/unit/utils/test_validate.py b/armada/tests/unit/utils/test_validate.py new file mode 100644 index 00000000..f30d6171 --- /dev/null +++ b/armada/tests/unit/utils/test_validate.py @@ -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) diff --git a/armada/utils/lint.py b/armada/utils/lint.py deleted file mode 100644 index 05ac5372..00000000 --- a/armada/utils/lint.py +++ /dev/null @@ -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) diff --git a/armada/utils/validate.py b/armada/utils/validate.py new file mode 100644 index 00000000..c7604a09 --- /dev/null +++ b/armada/utils/validate.py @@ -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', '') + 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() diff --git a/examples/keystone-manifest.yaml b/examples/keystone-manifest.yaml index 73fe7c19..4047ebbd 100644 --- a/examples/keystone-manifest.yaml +++ b/examples/keystone-manifest.yaml @@ -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 diff --git a/requirements.txt b/requirements.txt index b1976d8a..47794127 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ requests supermutes==0.2.5 Paste>=2.0.3 PasteDeploy>=1.5.2 +jsonschema>=2.6.0 # API falcon