From 4a8b2947caae7151db39a425e71f0f66dfd060ea Mon Sep 17 00:00:00 2001 From: Mazz Mosley Date: Wed, 26 Aug 2015 13:23:29 +0100 Subject: [PATCH] Interpolate extended config This refactoring is now really coming together. Construction is happening in the __init__, which is a constructor and helps clean up the design and clarity of intent of the code. We can now see (nearly) everything that is being constructed when a ServiceLoader is created. It needs all of these data constructs to perform the domain logic and actions. Which are now clearer to see and moving more towards the principle of functions doing (mostly)one thing and function names being more descriptive. resolve_extends is now concerned with the resolving of extends, rather than the construction, validation, pre processing and *then* resolving of extends. Happy days :) Signed-off-by: Mazz Mosley --- compose/config/config.py | 44 ++++++++++--------- compose/config/validation.py | 8 ++++ .../extends/valid-interpolation-2.yml | 3 ++ .../fixtures/extends/valid-interpolation.yml | 5 +++ tests/unit/config_test.py | 11 +++++ 5 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 tests/fixtures/extends/valid-interpolation-2.yml create mode 100644 tests/fixtures/extends/valid-interpolation.yml diff --git a/compose/config/config.py b/compose/config/config.py index 65a5b5472..e3ba2aeb8 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -11,6 +11,7 @@ from .errors import ComposeFileNotFound from .errors import ConfigurationError from .interpolation import interpolate_environment_variables from .validation import validate_against_schema +from .validation import validate_extended_service_exists from .validation import validate_extends_file_path from .validation import validate_service_names from .validation import validate_top_level_object @@ -178,12 +179,25 @@ class ServiceLoader(object): self.service_dict['extends'], self.filename ) - self.extended_config_path = self.get_extended_config_path( self.service_dict['extends'] ) - extended_config = load_yaml(self.extended_config_path) - validate_against_schema(extended_config) + self.extended_service_name = self.service_dict['extends']['service'] + + full_extended_config = pre_process_config( + load_yaml(self.extended_config_path) + ) + + validate_extended_service_exists( + self.extended_service_name, + full_extended_config, + self.extended_config_path + ) + validate_against_schema(full_extended_config) + + self.extended_config = full_extended_config[self.extended_service_name] + else: + self.extended_config = None def detect_cycle(self, name): if self.signature(name) in self.already_seen: @@ -214,40 +228,28 @@ class ServiceLoader(object): self.service_dict['environment'] = env def resolve_extends(self): - if 'extends' not in self.service_dict: + if self.extended_config is None: return self.service_dict - extends_options = self.service_dict['extends'] service_name = self.service_dict['name'] - other_config_path = self.extended_config_path other_working_dir = os.path.dirname(self.extended_config_path) other_already_seen = self.already_seen + [self.signature(service_name)] - base_service = extends_options['service'] - other_config = load_yaml(other_config_path) - - if base_service not in other_config: - msg = ( - "Cannot extend service '%s' in %s: Service not found" - ) % (base_service, other_config_path) - raise ConfigurationError(msg) - - other_service_dict = other_config[base_service] other_loader = ServiceLoader( working_dir=other_working_dir, - filename=other_config_path, + filename=self.extended_config_path, service_name=service_name, - service_dict=other_service_dict, + service_dict=self.extended_config, already_seen=other_already_seen, ) - other_loader.detect_cycle(extends_options['service']) + other_loader.detect_cycle(self.extended_service_name) other_service_dict = other_loader.make_service_dict() validate_extended_service_dict( other_service_dict, - filename=other_config_path, - service=extends_options['service'], + filename=self.extended_config_path, + service=self.extended_service_name, ) return merge_service_dicts(other_service_dict, self.service_dict) diff --git a/compose/config/validation.py b/compose/config/validation.py index 1ae8981ca..304e7e760 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -79,6 +79,14 @@ def validate_extends_file_path(service_name, extends_options, filename): ) +def validate_extended_service_exists(extended_service_name, full_extended_config, extended_config_path): + if extended_service_name not in full_extended_config: + msg = ( + "Cannot extend service '%s' in %s: Service not found" + ) % (extended_service_name, extended_config_path) + raise ConfigurationError(msg) + + def get_unsupported_config_msg(service_name, error_key): msg = "Unsupported config option for '{}' service: '{}'".format(service_name, error_key) if error_key in DOCKER_CONFIG_HINTS: diff --git a/tests/fixtures/extends/valid-interpolation-2.yml b/tests/fixtures/extends/valid-interpolation-2.yml new file mode 100644 index 000000000..cb7bd93fc --- /dev/null +++ b/tests/fixtures/extends/valid-interpolation-2.yml @@ -0,0 +1,3 @@ +web: + build: '.' + hostname: "host-${HOSTNAME_VALUE}" diff --git a/tests/fixtures/extends/valid-interpolation.yml b/tests/fixtures/extends/valid-interpolation.yml new file mode 100644 index 000000000..68e8740fb --- /dev/null +++ b/tests/fixtures/extends/valid-interpolation.yml @@ -0,0 +1,5 @@ +myweb: + extends: + service: web + file: valid-interpolation-2.yml + command: top diff --git a/tests/unit/config_test.py b/tests/unit/config_test.py index 98ae51385..7624bbdfd 100644 --- a/tests/unit/config_test.py +++ b/tests/unit/config_test.py @@ -914,6 +914,17 @@ class ExtendsTest(unittest.TestCase): with self.assertRaisesRegexp(ConfigurationError, expected_error_msg): load_from_filename('tests/fixtures/extends/invalid-net.yml') + @mock.patch.dict(os.environ) + def test_valid_interpolation_in_extended_service(self): + os.environ.update( + HOSTNAME_VALUE="penguin", + ) + expected_interpolated_value = "host-penguin" + + service_dicts = load_from_filename('tests/fixtures/extends/valid-interpolation.yml') + for service in service_dicts: + self.assertTrue(service['hostname'], expected_interpolated_value) + def test_volume_path(self): dicts = load_from_filename('tests/fixtures/volume-path/docker-compose.yml')