# 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 collections.abc import json import yaml from armada.exceptions import override_exceptions from armada.exceptions import validate_exceptions from armada.handlers import schema from armada.utils import validate class Override(object): def __init__(self, documents, overrides=None, values=None): self.documents = documents self.overrides = overrides self.values = values def _load_yaml_file(self, doc): ''' Retrieve yaml file as a dictionary. ''' try: with open(doc) as f: return list(yaml.safe_load_all(f.read())) except IOError: raise override_exceptions.InvalidOverrideFileException(doc) def _document_checker(self, doc, ovr=None): # Validate document or raise the appropriate exception try: valid, details = validate.validate_armada_documents(doc) except (RuntimeError, TypeError): raise override_exceptions.InvalidOverrideValueException(ovr) if not valid: if ovr: raise override_exceptions.InvalidOverrideValueException(ovr) else: raise validate_exceptions.InvalidManifestException( error_messages=details) def update(self, d, u): for k, v in u.items(): if isinstance(v, collections.abc.Mapping): r = self.update(d.get(k, {}), v) d[k] = r elif isinstance(v, str) and isinstance(d.get(k), (list, tuple)): d[k] = [x.strip() for x in v.split(',')] else: d[k] = u[k] return d def find_document_type(self, alias): if alias == 'chart_group': return schema.TYPE_CHARTGROUP if alias == 'chart': return schema.TYPE_CHART if alias == 'manifest': return schema.TYPE_MANIFEST else: raise ValueError("Could not find {} document".format(alias)) def find_manifest_document(self, doc_path): for doc in self.documents: schema_info = schema.get_schema_info(doc.get('schema')) if schema_info.type == self.find_document_type( doc_path[0]) and doc.get('metadata', {}).get('name') == doc_path[1]: return doc raise override_exceptions.UnknownDocumentOverrideException( doc_path[0], doc_path[1]) def array_to_dict(self, data_path, new_value): # TODO(fmontei): Handle `json.decoder.JSONDecodeError` getting thrown # better. def convert(data): if isinstance(data, str): return str(data) elif isinstance(data, collections.abc.Mapping): return dict(map(convert, data.items())) elif isinstance(data, collections.abc.Iterable): return type(data)(map(convert, data)) else: return data if not new_value: return if not data_path: return tree = {} t = tree for part in data_path: if part == data_path[-1]: t.setdefault(part, None) continue t = t.setdefault(part, {}) string = json.dumps(tree).replace('null', '"{}"'.format(new_value)) data_obj = convert(json.loads(string)) return data_obj def override_manifest_value(self, doc_path, data_path, new_value): document = self.find_manifest_document(doc_path) new_data = self.array_to_dict(data_path, new_value) self.update(document.get('data', {}), new_data) def update_documents(self, merging_values): for doc in merging_values: self.update_document(doc) def update_document(self, ovr): ovr_schema_info = schema.get_schema_info(ovr.get('schema')) if ovr_schema_info: for doc in self.documents: schema_info = schema.get_schema_info(doc.get('schema')) if schema_info: if schema_info == ovr_schema_info: if doc['metadata']['name'] == ovr['metadata']['name']: data = doc.get('data', {}) ovr_data = ovr.get('data', {}) self.update(data, ovr_data) return def update_manifests(self): if self.values: for value in self.values: merging_values = self._load_yaml_file(value) self.update_documents(merging_values) # Validate document with updated values self._document_checker(self.documents, self.values) if self.overrides: for override in self.overrides: new_value = override.split('=', 1)[1] doc_path = override.split('=', 1)[0].split(":") data_path = doc_path.pop().split('.') self.override_manifest_value(doc_path, data_path, new_value) # Validate document with overrides self._document_checker(self.documents, self.overrides) if not (self.values and self.overrides): # Valiate document self._document_checker(self.documents) return self.documents