From 4ecf5e01ff14b85e60eb2fe02aa15d41931528c2 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 27 Feb 2015 11:54:57 +0000 Subject: [PATCH] Extract YAML loading and parsing into config module Signed-off-by: Aanand Prasad --- compose/cli/command.py | 13 +-- compose/cli/main.py | 3 +- compose/config.py | 180 ++++++++++++++++++++++++++++++ compose/project.py | 19 +--- compose/service.py | 115 +------------------ tests/integration/project_test.py | 41 +++---- tests/integration/service_test.py | 15 +-- tests/integration/testcases.py | 8 +- tests/unit/config_test.py | 134 ++++++++++++++++++++++ tests/unit/project_test.py | 14 +-- tests/unit/service_test.py | 100 ----------------- 11 files changed, 358 insertions(+), 284 deletions(-) create mode 100644 compose/config.py create mode 100644 tests/unit/config_test.py diff --git a/compose/cli/command.py b/compose/cli/command.py index c26f3bc38..e829b25b2 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -4,9 +4,9 @@ from requests.exceptions import ConnectionError, SSLError import logging import os import re -import yaml import six +from .. import config from ..project import Project from ..service import ConfigError from .docopt_command import DocoptCommand @@ -69,18 +69,11 @@ class Command(DocoptCommand): return verbose_proxy.VerboseProxy('docker', client) return client - def get_config(self, config_path): - try: - with open(config_path, 'r') as fh: - return yaml.safe_load(fh) - except IOError as e: - raise errors.UserError(six.text_type(e)) - def get_project(self, config_path, project_name=None, verbose=False): try: - return Project.from_config( + return Project.from_dicts( self.get_project_name(config_path, project_name), - self.get_config(config_path), + config.load(config_path), self.get_client(verbose=verbose)) except ConfigError as e: raise errors.UserError(six.text_type(e)) diff --git a/compose/cli/main.py b/compose/cli/main.py index 434480b50..aafb199b7 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -12,7 +12,8 @@ import dockerpty from .. import __version__ from ..project import NoSuchService, ConfigurationError -from ..service import BuildError, CannotBeScaledError, parse_environment +from ..service import BuildError, CannotBeScaledError +from ..config import parse_environment from .command import Command from .docopt_command import NoSuchCommand from .errors import UserError diff --git a/compose/config.py b/compose/config.py new file mode 100644 index 000000000..4376d97cf --- /dev/null +++ b/compose/config.py @@ -0,0 +1,180 @@ +import os +import yaml +import six + + +DOCKER_CONFIG_KEYS = [ + 'cap_add', + 'cap_drop', + 'cpu_shares', + 'command', + 'detach', + 'dns', + 'dns_search', + 'domainname', + 'entrypoint', + 'env_file', + 'environment', + 'hostname', + 'image', + 'links', + 'mem_limit', + 'net', + 'ports', + 'privileged', + 'restart', + 'stdin_open', + 'tty', + 'user', + 'volumes', + 'volumes_from', + 'working_dir', +] + +ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [ + 'build', + 'expose', + 'external_links', + 'name', +] + +DOCKER_CONFIG_HINTS = { + 'cpu_share' : 'cpu_shares', + 'link' : 'links', + 'port' : 'ports', + 'privilege' : 'privileged', + 'priviliged': 'privileged', + 'privilige' : 'privileged', + 'volume' : 'volumes', + 'workdir' : 'working_dir', +} + + +def load(filename): + return from_dictionary(load_yaml(filename)) + + +def load_yaml(filename): + try: + with open(filename, 'r') as fh: + return yaml.safe_load(fh) + except IOError as e: + raise ConfigurationError(six.text_type(e)) + + +def from_dictionary(dictionary): + service_dicts = [] + + for service_name, service_dict in list(dictionary.items()): + if not isinstance(service_dict, dict): + raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your docker-compose.yml must map to a dictionary of configuration options.' % service_name) + service_dict = make_service_dict(service_name, service_dict) + service_dicts.append(service_dict) + + return service_dicts + + +def make_service_dict(name, options): + service_dict = options.copy() + service_dict['name'] = name + return process_container_options(service_dict) + + +def process_container_options(service_dict): + for k in service_dict: + if k not in ALLOWED_KEYS: + msg = "Unsupported config option for %s service: '%s'" % (service_dict['name'], k) + if k in DOCKER_CONFIG_HINTS: + msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k] + raise ConfigurationError(msg) + + for filename in get_env_files(service_dict): + if not os.path.exists(filename): + raise ConfigurationError("Couldn't find env file for service %s: %s" % (service_dict['name'], filename)) + + if 'environment' in service_dict or 'env_file' in service_dict: + service_dict['environment'] = build_environment(service_dict) + + return service_dict + + +def parse_links(links): + return dict(parse_link(l) for l in links) + + +def parse_link(link): + if ':' in link: + source, alias = link.split(':', 1) + return (alias, source) + else: + return (link, link) + + +def get_env_files(options): + env_files = options.get('env_file', []) + if not isinstance(env_files, list): + env_files = [env_files] + return env_files + + +def build_environment(options): + env = {} + + for f in get_env_files(options): + env.update(env_vars_from_file(f)) + + env.update(parse_environment(options.get('environment'))) + return dict(resolve_env(k, v) for k, v in six.iteritems(env)) + + +def parse_environment(environment): + if not environment: + return {} + + if isinstance(environment, list): + return dict(split_env(e) for e in environment) + + if isinstance(environment, dict): + return environment + + raise ConfigurationError( + "environment \"%s\" must be a list or mapping," % + environment + ) + + +def split_env(env): + if '=' in env: + return env.split('=', 1) + else: + return env, None + + +def resolve_env(key, val): + if val is not None: + return key, val + elif key in os.environ: + return key, os.environ[key] + else: + return key, '' + + +def env_vars_from_file(filename): + """ + Read in a line delimited file of environment variables. + """ + env = {} + for line in open(filename, 'r'): + line = line.strip() + if line and not line.startswith('#'): + k, v = split_env(line) + env[k] = v + return env + + +class ConfigurationError(Exception): + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return self.msg diff --git a/compose/project.py b/compose/project.py index 794ef2b65..881d8eb0a 100644 --- a/compose/project.py +++ b/compose/project.py @@ -3,6 +3,7 @@ from __future__ import absolute_import import logging from functools import reduce +from .config import ConfigurationError from .service import Service from .container import Container from docker.errors import APIError @@ -85,16 +86,6 @@ class Project(object): volumes_from=volumes_from, **service_dict)) return project - @classmethod - def from_config(cls, name, config, client): - dicts = [] - for service_name, service in list(config.items()): - if not isinstance(service, dict): - raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your docker-compose.yml must map to a dictionary of configuration options.' % service_name) - service['name'] = service_name - dicts.append(service) - return cls.from_dicts(name, dicts, client) - def get_service(self, name): """ Retrieve a service by name. Raises NoSuchService @@ -277,13 +268,5 @@ class NoSuchService(Exception): return self.msg -class ConfigurationError(Exception): - def __init__(self, msg): - self.msg = msg - - def __str__(self): - return self.msg - - class DependencyError(ConfigurationError): pass diff --git a/compose/service.py b/compose/service.py index 377198cf4..c65874c26 100644 --- a/compose/service.py +++ b/compose/service.py @@ -8,51 +8,14 @@ from operator import attrgetter import sys from docker.errors import APIError -import six +from .config import DOCKER_CONFIG_KEYS from .container import Container, get_container_name from .progress_stream import stream_output, StreamOutputError log = logging.getLogger(__name__) -DOCKER_CONFIG_KEYS = [ - 'cap_add', - 'cap_drop', - 'cpu_shares', - 'command', - 'detach', - 'dns', - 'dns_search', - 'domainname', - 'entrypoint', - 'env_file', - 'environment', - 'hostname', - 'image', - 'mem_limit', - 'net', - 'ports', - 'privileged', - 'restart', - 'stdin_open', - 'tty', - 'user', - 'volumes', - 'volumes_from', - 'working_dir', -] -DOCKER_CONFIG_HINTS = { - 'cpu_share' : 'cpu_shares', - 'link' : 'links', - 'port' : 'ports', - 'privilege' : 'privileged', - 'priviliged': 'privileged', - 'privilige' : 'privileged', - 'volume' : 'volumes', - 'workdir' : 'working_dir', -} - DOCKER_START_KEYS = [ 'cap_add', 'cap_drop', @@ -96,20 +59,6 @@ class Service(object): if 'image' in options and 'build' in options: raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) - for filename in get_env_files(options): - if not os.path.exists(filename): - raise ConfigError("Couldn't find env file for service %s: %s" % (name, filename)) - - supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose', - 'external_links'] - - for k in options: - if k not in supported_options: - msg = "Unsupported config option for %s service: '%s'" % (name, k) - if k in DOCKER_CONFIG_HINTS: - msg += " (did you mean '%s'?)" % DOCKER_CONFIG_HINTS[k] - raise ConfigError(msg) - self.name = name self.client = client self.project = project @@ -478,8 +427,6 @@ class Service(object): (parse_volume_spec(v).internal, {}) for v in container_options['volumes']) - container_options['environment'] = build_environment(container_options) - if self.can_be_built(): container_options['image'] = self.full_name else: @@ -648,63 +595,3 @@ def split_port(port): external_ip, external_port, internal_port = parts return internal_port, (external_ip, external_port or None) - - -def get_env_files(options): - env_files = options.get('env_file', []) - if not isinstance(env_files, list): - env_files = [env_files] - return env_files - - -def build_environment(options): - env = {} - - for f in get_env_files(options): - env.update(env_vars_from_file(f)) - - env.update(parse_environment(options.get('environment'))) - return dict(resolve_env(k, v) for k, v in six.iteritems(env)) - - -def parse_environment(environment): - if not environment: - return {} - - if isinstance(environment, list): - return dict(split_env(e) for e in environment) - - if isinstance(environment, dict): - return environment - - raise ConfigError("environment \"%s\" must be a list or mapping," % - environment) - - -def split_env(env): - if '=' in env: - return env.split('=', 1) - else: - return env, None - - -def resolve_env(key, val): - if val is not None: - return key, val - elif key in os.environ: - return key, os.environ[key] - else: - return key, '' - - -def env_vars_from_file(filename): - """ - Read in a line delimited file of environment variables. - """ - env = {} - for line in open(filename, 'r'): - line = line.strip() - if line and not line.startswith('#'): - k, v = split_env(line) - env[k] = v - return env diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 17b54daee..a46fc2f5a 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -1,14 +1,15 @@ from __future__ import unicode_literals -from compose.project import Project, ConfigurationError +from compose import config +from compose.project import Project from compose.container import Container from .testcases import DockerClientTestCase class ProjectTest(DockerClientTestCase): def test_volumes_from_service(self): - project = Project.from_config( + project = Project.from_dicts( name='composetest', - config={ + service_dicts=config.from_dictionary({ 'data': { 'image': 'busybox:latest', 'volumes': ['/var/data'], @@ -17,7 +18,7 @@ class ProjectTest(DockerClientTestCase): 'image': 'busybox:latest', 'volumes_from': ['data'], }, - }, + }), client=self.client, ) db = project.get_service('db') @@ -31,14 +32,14 @@ class ProjectTest(DockerClientTestCase): volumes=['/var/data'], name='composetest_data_container', ) - project = Project.from_config( + project = Project.from_dicts( name='composetest', - config={ + service_dicts=config.from_dictionary({ 'db': { 'image': 'busybox:latest', 'volumes_from': ['composetest_data_container'], }, - }, + }), client=self.client, ) db = project.get_service('db') @@ -48,9 +49,9 @@ class ProjectTest(DockerClientTestCase): project.remove_stopped() def test_net_from_service(self): - project = Project.from_config( + project = Project.from_dicts( name='composetest', - config={ + service_dicts=config.from_dictionary({ 'net': { 'image': 'busybox:latest', 'command': ["/bin/sleep", "300"] @@ -59,8 +60,8 @@ class ProjectTest(DockerClientTestCase): 'image': 'busybox:latest', 'net': 'container:net', 'command': ["/bin/sleep", "300"] - }, - }, + }, + }), client=self.client, ) @@ -82,14 +83,14 @@ class ProjectTest(DockerClientTestCase): ) net_container.start() - project = Project.from_config( + project = Project.from_dicts( name='composetest', - config={ + service_dicts=config.from_dictionary({ 'web': { 'image': 'busybox:latest', 'net': 'container:composetest_net_container' }, - }, + }), client=self.client, ) @@ -257,9 +258,9 @@ class ProjectTest(DockerClientTestCase): project.remove_stopped() def test_project_up_starts_depends(self): - project = Project.from_config( + project = Project.from_dicts( name='composetest', - config={ + service_dicts=config.from_dictionary({ 'console': { 'image': 'busybox:latest', 'command': ["/bin/sleep", "300"], @@ -279,7 +280,7 @@ class ProjectTest(DockerClientTestCase): 'net': 'container:net', 'links': ['app'] }, - }, + }), client=self.client, ) project.start() @@ -296,9 +297,9 @@ class ProjectTest(DockerClientTestCase): project.remove_stopped() def test_project_up_with_no_deps(self): - project = Project.from_config( + project = Project.from_dicts( name='composetest', - config={ + service_dicts=config.from_dictionary({ 'console': { 'image': 'busybox:latest', 'command': ["/bin/sleep", "300"], @@ -324,7 +325,7 @@ class ProjectTest(DockerClientTestCase): 'links': ['app'], 'volumes_from': ['vol'] }, - }, + }), client=self.client, ) project.start() diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 7b95b870f..8008fbbca 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from __future__ import absolute_import import os from os import path +import mock from compose import Service from compose.service import CannotBeScaledError @@ -481,16 +482,12 @@ class ServiceTest(DockerClientTestCase): for k,v in {'ONE': '1', 'TWO': '2', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'}.items(): self.assertEqual(env[k], v) + @mock.patch.dict(os.environ) def test_resolve_env(self): - service = self.create_service('web', environment={'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': None, 'NO_DEF': None}) os.environ['FILE_DEF'] = 'E1' os.environ['FILE_DEF_EMPTY'] = 'E2' os.environ['ENV_DEF'] = 'E3' - try: - env = create_and_start_container(service).environment - for k,v in {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}.items(): - self.assertEqual(env[k], v) - finally: - del os.environ['FILE_DEF'] - del os.environ['FILE_DEF_EMPTY'] - del os.environ['ENV_DEF'] + service = self.create_service('web', environment={'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': None, 'NO_DEF': None}) + env = create_and_start_container(service).environment + for k,v in {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}.items(): + self.assertEqual(env[k], v) diff --git a/tests/integration/testcases.py b/tests/integration/testcases.py index 53882561b..4f49124cf 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from __future__ import absolute_import from compose.service import Service +from compose.config import make_service_dict from compose.cli.docker_client import docker_client from compose.progress_stream import stream_output from .. import unittest @@ -21,14 +22,15 @@ class DockerClientTestCase(unittest.TestCase): self.client.remove_image(i) def create_service(self, name, **kwargs): + kwargs['image'] = "busybox:latest" + if 'command' not in kwargs: kwargs['command'] = ["/bin/sleep", "300"] + return Service( project='composetest', - name=name, client=self.client, - image="busybox:latest", - **kwargs + **make_service_dict(name, kwargs) ) def check_build(self, *args, **kwargs): diff --git a/tests/unit/config_test.py b/tests/unit/config_test.py new file mode 100644 index 000000000..8f59694de --- /dev/null +++ b/tests/unit/config_test.py @@ -0,0 +1,134 @@ +import os +import mock +from .. import unittest + +from compose import config + +class ConfigTest(unittest.TestCase): + def test_from_dictionary(self): + service_dicts = config.from_dictionary({ + 'foo': {'image': 'busybox'}, + 'bar': {'environment': ['FOO=1']}, + }) + + self.assertEqual( + sorted(service_dicts, key=lambda d: d['name']), + sorted([ + { + 'name': 'bar', + 'environment': {'FOO': '1'}, + }, + { + 'name': 'foo', + 'image': 'busybox', + } + ]) + ) + + def test_from_dictionary_throws_error_when_not_dict(self): + with self.assertRaises(config.ConfigurationError): + config.from_dictionary({ + 'web': 'busybox:latest', + }) + + def test_config_validation(self): + self.assertRaises( + config.ConfigurationError, + lambda: config.make_service_dict('foo', {'port': ['8000']}) + ) + config.make_service_dict('foo', {'ports': ['8000']}) + + def test_parse_environment_as_list(self): + environment =[ + 'NORMAL=F1', + 'CONTAINS_EQUALS=F=2', + 'TRAILING_EQUALS=', + ] + self.assertEqual( + config.parse_environment(environment), + {'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''}, + ) + + def test_parse_environment_as_dict(self): + environment = { + 'NORMAL': 'F1', + 'CONTAINS_EQUALS': 'F=2', + 'TRAILING_EQUALS': None, + } + self.assertEqual(config.parse_environment(environment), environment) + + def test_parse_environment_invalid(self): + with self.assertRaises(config.ConfigurationError): + config.parse_environment('a=b') + + def test_parse_environment_empty(self): + self.assertEqual(config.parse_environment(None), {}) + + @mock.patch.dict(os.environ) + def test_resolve_environment(self): + os.environ['FILE_DEF'] = 'E1' + os.environ['FILE_DEF_EMPTY'] = 'E2' + os.environ['ENV_DEF'] = 'E3' + + service_dict = config.make_service_dict( + 'foo', + { + 'environment': { + 'FILE_DEF': 'F1', + 'FILE_DEF_EMPTY': '', + 'ENV_DEF': None, + 'NO_DEF': None + }, + }, + ) + + self.assertEqual( + service_dict['environment'], + {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}, + ) + + def test_env_from_file(self): + service_dict = config.make_service_dict( + 'foo', + {'env_file': 'tests/fixtures/env/one.env'}, + ) + self.assertEqual( + service_dict['environment'], + {'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'bar'}, + ) + + def test_env_from_multiple_files(self): + service_dict = config.make_service_dict( + 'foo', + { + 'env_file': [ + 'tests/fixtures/env/one.env', + 'tests/fixtures/env/two.env', + ], + }, + ) + self.assertEqual( + service_dict['environment'], + {'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'}, + ) + + def test_env_nonexistent_file(self): + options = {'env_file': 'tests/fixtures/env/nonexistent.env'} + self.assertRaises( + config.ConfigurationError, + lambda: config.make_service_dict('foo', options), + ) + + @mock.patch.dict(os.environ) + def test_resolve_environment_from_file(self): + os.environ['FILE_DEF'] = 'E1' + os.environ['FILE_DEF_EMPTY'] = 'E2' + os.environ['ENV_DEF'] = 'E3' + service_dict = config.make_service_dict( + 'foo', + {'env_file': 'tests/fixtures/env/resolve.env'}, + ) + self.assertEqual( + service_dict['environment'], + {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}, + ) diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index b06e14e58..c995d432f 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals from .. import unittest from compose.service import Service -from compose.project import Project, ConfigurationError +from compose.project import Project from compose.container import Container +from compose import config import mock import docker @@ -49,26 +50,21 @@ class ProjectTest(unittest.TestCase): self.assertEqual(project.services[2].name, 'web') def test_from_config(self): - project = Project.from_config('composetest', { + dicts = config.from_dictionary({ 'web': { 'image': 'busybox:latest', }, 'db': { 'image': 'busybox:latest', }, - }, None) + }) + project = Project.from_dicts('composetest', dicts, None) self.assertEqual(len(project.services), 2) self.assertEqual(project.get_service('web').name, 'web') self.assertEqual(project.get_service('web').options['image'], 'busybox:latest') self.assertEqual(project.get_service('db').name, 'db') self.assertEqual(project.get_service('db').options['image'], 'busybox:latest') - def test_from_config_throws_error_when_not_dict(self): - with self.assertRaises(ConfigurationError): - project = Project.from_config('composetest', { - 'web': 'busybox:latest', - }, None) - def test_get_service(self): web = Service( project='composetest', diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 012a51ab6..c70c30bfa 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -16,7 +16,6 @@ from compose.service import ( build_port_bindings, build_volume_binding, get_container_name, - parse_environment, parse_repository_tag, parse_volume_spec, split_port, @@ -47,10 +46,6 @@ class ServiceTest(unittest.TestCase): self.assertRaises(ConfigError, lambda: Service(name='foo', project='_')) Service(name='foo', project='bar') - def test_config_validation(self): - self.assertRaises(ConfigError, lambda: Service(name='foo', port=['8000'])) - Service(name='foo', ports=['8000']) - def test_get_container_name(self): self.assertIsNone(get_container_name({})) self.assertEqual(get_container_name({'Name': 'myproject_db_1'}), 'myproject_db_1') @@ -321,98 +316,3 @@ class ServiceVolumesTest(unittest.TestCase): binding, ('/home/user', dict(bind='/home/user', ro=False))) -class ServiceEnvironmentTest(unittest.TestCase): - - def setUp(self): - self.mock_client = mock.create_autospec(docker.Client) - self.mock_client.containers.return_value = [] - - def test_parse_environment_as_list(self): - environment =[ - 'NORMAL=F1', - 'CONTAINS_EQUALS=F=2', - 'TRAILING_EQUALS=' - ] - self.assertEqual( - parse_environment(environment), - {'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''}) - - def test_parse_environment_as_dict(self): - environment = { - 'NORMAL': 'F1', - 'CONTAINS_EQUALS': 'F=2', - 'TRAILING_EQUALS': None, - } - self.assertEqual(parse_environment(environment), environment) - - def test_parse_environment_invalid(self): - with self.assertRaises(ConfigError): - parse_environment('a=b') - - def test_parse_environment_empty(self): - self.assertEqual(parse_environment(None), {}) - - @mock.patch.dict(os.environ) - def test_resolve_environment(self): - os.environ['FILE_DEF'] = 'E1' - os.environ['FILE_DEF_EMPTY'] = 'E2' - os.environ['ENV_DEF'] = 'E3' - service = Service( - 'foo', - environment={ - 'FILE_DEF': 'F1', - 'FILE_DEF_EMPTY': '', - 'ENV_DEF': None, - 'NO_DEF': None - }, - client=self.mock_client, - image='image_name', - ) - options = service._get_container_create_options({}) - self.assertEqual( - options['environment'], - {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''} - ) - - def test_env_from_file(self): - service = Service('foo', - env_file='tests/fixtures/env/one.env', - client=self.mock_client, - image='image_name', - ) - options = service._get_container_create_options({}) - self.assertEqual( - options['environment'], - {'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'bar'} - ) - - def test_env_from_multiple_files(self): - service = Service('foo', - env_file=['tests/fixtures/env/one.env', 'tests/fixtures/env/two.env'], - client=self.mock_client, - image='image_name', - ) - options = service._get_container_create_options({}) - self.assertEqual( - options['environment'], - {'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'} - ) - - def test_env_nonexistent_file(self): - self.assertRaises(ConfigError, lambda: Service('foo', env_file='tests/fixtures/env/nonexistent.env')) - - @mock.patch.dict(os.environ) - def test_resolve_environment_from_file(self): - os.environ['FILE_DEF'] = 'E1' - os.environ['FILE_DEF_EMPTY'] = 'E2' - os.environ['ENV_DEF'] = 'E3' - service = Service('foo', - env_file=['tests/fixtures/env/resolve.env'], - client=self.mock_client, - image='image_name', - ) - options = service._get_container_create_options({}) - self.assertEqual( - options['environment'], - {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''} - )