# 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. from copy import deepcopy from oslo_log import log as logging from armada import const from armada import exceptions from armada.handlers import schema LOG = logging.getLogger(__name__) class Manifest(object): def __init__(self, documents, target_manifest=None): """Instantiates a Manifest object. An Armada Manifest expects that at least one of each of the following be included in ``documents``: * A document with schema "armada/Chart/v1" * A document with schema "armada/ChartGroup/v1" And only one document of the following is allowed: * A document with schema "armada/Manifest/v1" If multiple documents with schema "armada/Manifest/v1" are provided, specify ``target_manifest`` to select the target one. :param List[dict] documents: Documents out of which to build the Armada Manifest. :param str target_manifest: The target manifest to use when multiple documents with "armada/Manifest/v1" are contained in ``documents``. Default is None. :raises ManifestException: If the expected number of document types are not found or if the document types are missing required properties. """ self.documents = deepcopy(documents) self.charts, self.groups, manifests = self._find_documents( target_manifest) if len(manifests) > 1: error = ('Multiple manifests are not supported. Ensure that the ' '`target_manifest` option is set to specify the target ' 'manifest') LOG.error(error) raise exceptions.ManifestException(details=error) else: self.manifest = manifests[0] if manifests else None if not all([self.charts, self.groups, self.manifest]): expected_schemas = [schema.TYPE_CHART, schema.TYPE_CHARTGROUP] error = ('Documents must include at least one of each of {} ' 'and only one {}').format(expected_schemas, schema.TYPE_MANIFEST) LOG.error(error) raise exceptions.ManifestException(details=error) def _find_documents(self, target_manifest=None): """Returns the chart documents, chart group documents, and Armada manifest If multiple documents with schema "armada/Manifest/v1" are provided, specify ``target_manifest`` to select the target one. :param str target_manifest: The target manifest to use when multiple documents with "armada/Manifest/v1" are contained in ``documents``. Default is None. :returns: Tuple of chart documents, chart groups, and manifests found in ``self.documents`` :rtype: tuple """ charts = [] groups = [] manifests = [] for document in self.documents: schema_info = schema.get_schema_info(document.get('schema')) if not schema_info: continue if schema_info.type == schema.TYPE_CHART: charts.append(document) if schema_info.type == schema.TYPE_CHARTGROUP: groups.append(document) if schema_info.type == schema.TYPE_MANIFEST: manifest_name = document.get('metadata', {}).get('name') if target_manifest: if manifest_name == target_manifest: manifests.append(document) else: manifests.append(document) return charts, groups, manifests def find_chart_document(self, name): """Returns a chart document with the specified name :param str name: name of the desired chart document :returns: The requested chart document :rtype: dict :raises ManifestException: If a chart document with the specified name is not found """ for chart in self.charts: if chart.get('metadata', {}).get('name') == name: return chart raise exceptions.BuildChartException( details='Could not find {} named "{}"'.format( schema.TYPE_CHART, name)) def find_chart_group_document(self, name): """Returns a chart group document with the specified name :param str name: name of the desired chart group document :returns: The requested chart group document :rtype: dict :raises ManifestException: If a chart group document with the specified name is not found """ for group in self.groups: if group.get('metadata', {}).get('name') == name: return group raise exceptions.BuildChartGroupException( details='Could not find {} named "{}"'.format( schema.TYPE_CHARTGROUP, name)) def build_chart_deps(self, chart): """Recursively build chart dependencies for ``chart``. :param dict chart: The chart whose dependencies will be recursively built. :returns: The chart with all dependencies. :rtype: dict :raises ManifestException: If a chart for a dependency name listed under ``chart['data']['dependencies']`` could not be found. """ try: chart_dependencies = chart.get(const.KEYWORD_DATA, {}).get( 'dependencies', []) for iter, dep in enumerate(chart_dependencies): if isinstance(dep, dict): continue chart_dep = self.find_chart_document(dep) self.build_chart_deps(chart_dep) chart[const.KEYWORD_DATA]['dependencies'][iter] = chart_dep except Exception: raise exceptions.ChartDependencyException( details='Could not build dependencies for {} named "{}"'. format(schema.TYPE_CHART, chart.get('metadata').get('name'))) else: return chart def build_chart_group(self, chart_group): """Builds the chart dependencies for`charts`chart group``. :param dict chart_group: The chart_group whose dependencies will be built. :returns: The chart_group with all dependencies. :rtype: dict :raises ManifestException: If a chart for a dependency name listed under ``chart_group['data']['chart_group']`` could not be found. """ try: chart = None for iter, chart in enumerate( chart_group.get(const.KEYWORD_DATA).get( const.KEYWORD_CHARTS, [])): if isinstance(chart, dict): continue chart_object = self.find_chart_document(chart) self.build_chart_deps(chart_object) chart_group[const.KEYWORD_DATA][const.KEYWORD_CHARTS][iter] = \ chart_object except exceptions.ManifestException: cg_name = chart_group.get('metadata', {}).get('name') raise exceptions.BuildChartGroupException( details='Could not build {} named "{}"'.format( schema.TYPE_CHARTGROUP, cg_name)) return chart_group def build_armada_manifest(self): """Builds the Armada manifest while pulling out data from the chart_group. :returns: The Armada manifest with the data of the chart groups. :rtype: dict :raises ManifestException: If a chart group's data listed under ``chart_group[const.KEYWORD_DATA]`` could not be found. """ for iter, group in enumerate( self.manifest.get(const.KEYWORD_DATA, {}).get( const.KEYWORD_GROUPS, [])): if isinstance(group, dict): continue chart_grp = self.find_chart_group_document(group) self.build_chart_group(chart_grp) self.manifest[const.KEYWORD_DATA][const.KEYWORD_GROUPS][iter] = \ chart_grp return self.manifest def get_manifest(self): """Builds the Armada manifest :returns: The Armada manifest. :rtype: dict """ self.build_armada_manifest() return self.manifest