From abc80a8c5941f3a9d15497f1a88ee02eee92b21c Mon Sep 17 00:00:00 2001 From: Claudius Heine Date: Wed, 21 Jun 2017 13:33:00 +0200 Subject: [PATCH] Implement file version and a check when using it If the configuration file structure changes over time, it might introduce incompatibilities. This patch introduces versioning of configuration file. Configuration files needs to add a 'version' entry into their 'header' like this: header: version: '0.9' After loading this file, the version is checked agains kas version. Every version 'M.m.p' of kas is allowed to load configuration files with version 'M.m' and all versions backwards up to and including the '__compatible_version__' from kas/__version__.py Signed-off-by: Claudius Heine --- kas/__init__.py | 1 + kas/__version__.py | 1 + kas/includehandler.py | 74 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/kas/__init__.py b/kas/__init__.py index a6225fd..645b095 100644 --- a/kas/__init__.py +++ b/kas/__init__.py @@ -24,6 +24,7 @@ """ from .__version__ import __version__ +from .__version__ import __compatible_version__ __license__ = 'MIT' __copyright__ = 'Copyright (c) Siemens AG, 2017' diff --git a/kas/__version__.py b/kas/__version__.py index e90d963..eb87993 100644 --- a/kas/__version__.py +++ b/kas/__version__.py @@ -26,3 +26,4 @@ __license__ = 'MIT' __copyright__ = 'Copyright (c) Siemens AG, 2017' __version__ = '0.9.0' +__compatible_version__ = '0.9' diff --git a/kas/includehandler.py b/kas/includehandler.py index 32b119e..1f1065a 100644 --- a/kas/includehandler.py +++ b/kas/includehandler.py @@ -30,11 +30,70 @@ import sys import collections import functools import logging +from distutils.version import StrictVersion + +from . import __version__, __compatible_version__ __license__ = 'MIT' __copyright__ = 'Copyright (c) Siemens AG, 2017' +def load_config(filename): + """ + Load the configuration file and test if version is supported. + """ + (_, ext) = os.path.splitext(filename) + config = None + if ext == '.json': + import json + with open(filename, 'rb') as fds: + config = json.load(fds) + elif ext == '.yml': + import yaml + with open(filename, 'rb') as fds: + config = yaml.safe_load(fds) + else: + logging.error('Config file extension not recognized: %s', + filename) + sys.exit(1) + + file_version_string = config.get('header', {}).get('version', None) + + if file_version_string is None: + logging.error('Version missing: %s', filename) + sys.exit(1) + + try: + if not isinstance(file_version_string, str): + logging.error('Version has to be a string: %s', + filename) + sys.exit(1) + + file_version = StrictVersion() + file_version.parse(file_version_string) + kas_version = StrictVersion() + kas_version.parse(__version__) + lower_version = StrictVersion() + lower_version.parse(__compatible_version__) + + # Remove patch version, because we provide limited forwards + # compatibility: + if file_version.version[2] > 0: + file_version.prerelease = None + file_version.version = tuple(list(file_version.version[:2]) + [0]) + + if file_version < lower_version or kas_version < file_version: + logging.error('This version of kas is compatible with version %s ' + 'to %s, file has version %s: %s', + lower_version, kas_version, file_version, filename) + sys.exit(1) + except ValueError: + logging.exception('Not expected version format: %s', filename) + raise + + return config + + class IncludeException(Exception): """ Class for exceptions that appear in the include mechanism. @@ -88,19 +147,6 @@ class GlobalIncludes(IncludeHandler): def get_config(self, repos=None): repos = repos or {} - def _internal_file_parser(filename): - (_, ext) = os.path.splitext(filename) - if ext == '.json': - import json - with open(filename, 'rb') as fds: - return json.load(fds) - elif ext == '.yml': - import yaml - with open(filename, 'rb') as fds: - return yaml.safe_load(fds) - logging.error('Config file extension not recognized: %s', ext) - sys.exit(1) - def _internal_include_handler(filename): """ Recursively load include files and find missing repos. @@ -129,7 +175,7 @@ class GlobalIncludes(IncludeHandler): """ missing_repos = [] configs = [] - current_config = _internal_file_parser(filename) + current_config = load_config(filename) if not isinstance(current_config, collections.Mapping): raise IncludeException('Configuration file does not contain a ' 'dictionary as base type')