|
|
@@ -13,7 +13,6 @@ from .errors import ConfigurationError
|
|
|
from .interpolation import interpolate_environment_variables
|
|
|
from .validation import validate_against_fields_schema
|
|
|
from .validation import validate_against_service_schema
|
|
|
-from .validation import validate_extended_service_exists
|
|
|
from .validation import validate_extends_file_path
|
|
|
from .validation import validate_top_level_object
|
|
|
|
|
|
@@ -99,6 +98,10 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
|
|
|
:type config: :class:`dict`
|
|
|
"""
|
|
|
|
|
|
+ @classmethod
|
|
|
+ def from_filename(cls, filename):
|
|
|
+ return cls(filename, load_yaml(filename))
|
|
|
+
|
|
|
|
|
|
def find(base_dir, filenames):
|
|
|
if filenames == ['-']:
|
|
|
@@ -114,7 +117,7 @@ def find(base_dir, filenames):
|
|
|
log.debug("Using configuration files: {}".format(",".join(filenames)))
|
|
|
return ConfigDetails(
|
|
|
os.path.dirname(filenames[0]),
|
|
|
- [ConfigFile(f, load_yaml(f)) for f in filenames])
|
|
|
+ [ConfigFile.from_filename(f) for f in filenames])
|
|
|
|
|
|
|
|
|
def get_default_config_files(base_dir):
|
|
|
@@ -174,21 +177,19 @@ def load(config_details):
|
|
|
"""
|
|
|
|
|
|
def build_service(filename, service_name, service_dict):
|
|
|
- loader = ServiceLoader(
|
|
|
+ resolver = ServiceExtendsResolver(
|
|
|
config_details.working_dir,
|
|
|
filename,
|
|
|
service_name,
|
|
|
service_dict)
|
|
|
- service_dict = loader.make_service_dict()
|
|
|
+ service_dict = process_service(config_details.working_dir, resolver.run())
|
|
|
validate_paths(service_dict)
|
|
|
return service_dict
|
|
|
|
|
|
- def load_file(filename, config):
|
|
|
- processed_config = interpolate_environment_variables(config)
|
|
|
- validate_against_fields_schema(processed_config)
|
|
|
+ def build_services(filename, config):
|
|
|
return [
|
|
|
build_service(filename, name, service_config)
|
|
|
- for name, service_config in processed_config.items()
|
|
|
+ for name, service_config in config.items()
|
|
|
]
|
|
|
|
|
|
def merge_services(base, override):
|
|
|
@@ -200,19 +201,30 @@ def load(config_details):
|
|
|
for name in all_service_names
|
|
|
}
|
|
|
|
|
|
- config_file = config_details.config_files[0]
|
|
|
- validate_top_level_object(config_file.config)
|
|
|
+ config_file = process_config_file(config_details.config_files[0])
|
|
|
for next_file in config_details.config_files[1:]:
|
|
|
- validate_top_level_object(next_file.config)
|
|
|
+ next_file = process_config_file(next_file)
|
|
|
|
|
|
- config_file = ConfigFile(
|
|
|
- config_file.filename,
|
|
|
- merge_services(config_file.config, next_file.config))
|
|
|
+ config = merge_services(config_file.config, next_file.config)
|
|
|
+ config_file = config_file._replace(config=config)
|
|
|
|
|
|
- return load_file(config_file.filename, config_file.config)
|
|
|
+ return build_services(config_file.filename, config_file.config)
|
|
|
|
|
|
|
|
|
-class ServiceLoader(object):
|
|
|
+def process_config_file(config_file, service_name=None):
|
|
|
+ validate_top_level_object(config_file.config)
|
|
|
+ processed_config = interpolate_environment_variables(config_file.config)
|
|
|
+ validate_against_fields_schema(processed_config)
|
|
|
+
|
|
|
+ if service_name and service_name not in processed_config:
|
|
|
+ raise ConfigurationError(
|
|
|
+ "Cannot extend service '{}' in {}: Service not found".format(
|
|
|
+ service_name, config_file.filename))
|
|
|
+
|
|
|
+ return config_file._replace(config=processed_config)
|
|
|
+
|
|
|
+
|
|
|
+class ServiceExtendsResolver(object):
|
|
|
def __init__(
|
|
|
self,
|
|
|
working_dir,
|
|
|
@@ -222,7 +234,7 @@ class ServiceLoader(object):
|
|
|
already_seen=None
|
|
|
):
|
|
|
if working_dir is None:
|
|
|
- raise ValueError("No working_dir passed to ServiceLoader()")
|
|
|
+ raise ValueError("No working_dir passed to ServiceExtendsResolver()")
|
|
|
|
|
|
self.working_dir = os.path.abspath(working_dir)
|
|
|
|
|
|
@@ -235,11 +247,17 @@ class ServiceLoader(object):
|
|
|
self.service_name = service_name
|
|
|
self.service_dict['name'] = service_name
|
|
|
|
|
|
- def detect_cycle(self, name):
|
|
|
- if self.signature(name) in self.already_seen:
|
|
|
- raise CircularReference(self.already_seen + [self.signature(name)])
|
|
|
+ @property
|
|
|
+ def signature(self):
|
|
|
+ return self.filename, self.service_name
|
|
|
+
|
|
|
+ def detect_cycle(self):
|
|
|
+ if self.signature in self.already_seen:
|
|
|
+ raise CircularReference(self.already_seen + [self.signature])
|
|
|
+
|
|
|
+ def run(self):
|
|
|
+ self.detect_cycle()
|
|
|
|
|
|
- def make_service_dict(self):
|
|
|
service_dict = dict(self.service_dict)
|
|
|
env = resolve_environment(self.working_dir, self.service_dict)
|
|
|
if env:
|
|
|
@@ -252,45 +270,32 @@ class ServiceLoader(object):
|
|
|
if not self.already_seen:
|
|
|
validate_against_service_schema(service_dict, self.service_name)
|
|
|
|
|
|
- return process_container_options(self.working_dir, service_dict)
|
|
|
+ return service_dict
|
|
|
|
|
|
def validate_and_construct_extends(self):
|
|
|
extends = self.service_dict['extends']
|
|
|
if not isinstance(extends, dict):
|
|
|
extends = {'service': extends}
|
|
|
|
|
|
- validate_extends_file_path(self.service_name, extends, self.filename)
|
|
|
config_path = self.get_extended_config_path(extends)
|
|
|
service_name = extends['service']
|
|
|
|
|
|
- config = load_yaml(config_path)
|
|
|
- validate_top_level_object(config)
|
|
|
- full_extended_config = interpolate_environment_variables(config)
|
|
|
-
|
|
|
- validate_extended_service_exists(
|
|
|
- service_name,
|
|
|
- full_extended_config,
|
|
|
- config_path
|
|
|
- )
|
|
|
- validate_against_fields_schema(full_extended_config)
|
|
|
-
|
|
|
- service_config = full_extended_config[service_name]
|
|
|
+ extended_file = process_config_file(
|
|
|
+ ConfigFile.from_filename(config_path),
|
|
|
+ service_name=service_name)
|
|
|
+ service_config = extended_file.config[service_name]
|
|
|
return config_path, service_config, service_name
|
|
|
|
|
|
def resolve_extends(self, extended_config_path, service_config, service_name):
|
|
|
- other_working_dir = os.path.dirname(extended_config_path)
|
|
|
- other_already_seen = self.already_seen + [self.signature(self.service_name)]
|
|
|
-
|
|
|
- other_loader = ServiceLoader(
|
|
|
- other_working_dir,
|
|
|
+ resolver = ServiceExtendsResolver(
|
|
|
+ os.path.dirname(extended_config_path),
|
|
|
extended_config_path,
|
|
|
- self.service_name,
|
|
|
+ service_name,
|
|
|
service_config,
|
|
|
- already_seen=other_already_seen,
|
|
|
+ already_seen=self.already_seen + [self.signature],
|
|
|
)
|
|
|
|
|
|
- other_loader.detect_cycle(service_name)
|
|
|
- other_service_dict = other_loader.make_service_dict()
|
|
|
+ other_service_dict = process_service(resolver.working_dir, resolver.run())
|
|
|
validate_extended_service_dict(
|
|
|
other_service_dict,
|
|
|
extended_config_path,
|
|
|
@@ -304,13 +309,11 @@ class ServiceLoader(object):
|
|
|
need to obtain a full path too or we are extending from a service
|
|
|
defined in our own file.
|
|
|
"""
|
|
|
+ validate_extends_file_path(self.service_name, extends_options, self.filename)
|
|
|
if 'file' in extends_options:
|
|
|
return expand_path(self.working_dir, extends_options['file'])
|
|
|
return self.filename
|
|
|
|
|
|
- def signature(self, name):
|
|
|
- return self.filename, name
|
|
|
-
|
|
|
|
|
|
def resolve_environment(working_dir, service_dict):
|
|
|
"""Unpack any environment variables from an env_file, if set.
|
|
|
@@ -354,7 +357,7 @@ def validate_ulimits(ulimit_config):
|
|
|
"than 'hard' value".format(ulimit_config))
|
|
|
|
|
|
|
|
|
-def process_container_options(working_dir, service_dict):
|
|
|
+def process_service(working_dir, service_dict):
|
|
|
service_dict = dict(service_dict)
|
|
|
|
|
|
if 'volumes' in service_dict and service_dict.get('volume_driver') is None:
|