Merge pull request #1099 from aanand/fix-env-file-resolution

Fix env file resolution
This commit is contained in:
Ben Firshman 2015-03-13 16:49:10 +00:00
commit dae451019b
15 changed files with 403 additions and 285 deletions

View File

@ -4,9 +4,9 @@ from requests.exceptions import ConnectionError, SSLError
import logging import logging
import os import os
import re import re
import yaml
import six import six
from .. import config
from ..project import Project from ..project import Project
from ..service import ConfigError from ..service import ConfigError
from .docopt_command import DocoptCommand from .docopt_command import DocoptCommand
@ -69,18 +69,11 @@ class Command(DocoptCommand):
return verbose_proxy.VerboseProxy('docker', client) return verbose_proxy.VerboseProxy('docker', client)
return 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): def get_project(self, config_path, project_name=None, verbose=False):
try: try:
return Project.from_config( return Project.from_dicts(
self.get_project_name(config_path, project_name), self.get_project_name(config_path, project_name),
self.get_config(config_path), config.load(config_path),
self.get_client(verbose=verbose)) self.get_client(verbose=verbose))
except ConfigError as e: except ConfigError as e:
raise errors.UserError(six.text_type(e)) raise errors.UserError(six.text_type(e))

View File

@ -12,7 +12,8 @@ import dockerpty
from .. import __version__ from .. import __version__
from ..project import NoSuchService, ConfigurationError 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 .command import Command
from .docopt_command import NoSuchCommand from .docopt_command import NoSuchCommand
from .errors import UserError from .errors import UserError

198
compose/config.py Normal file
View File

@ -0,0 +1,198 @@
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):
working_dir = os.path.dirname(filename)
return from_dictionary(load_yaml(filename), working_dir=working_dir)
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, working_dir=None):
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, working_dir=working_dir)
service_dicts.append(service_dict)
return service_dicts
def make_service_dict(name, options, working_dir=None):
service_dict = options.copy()
service_dict['name'] = name
service_dict = resolve_environment(service_dict, working_dir=working_dir)
return process_container_options(service_dict, working_dir=working_dir)
def process_container_options(service_dict, working_dir=None):
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)
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, working_dir=None):
if 'env_file' not in options:
return {}
if working_dir is None:
raise Exception("No working_dir passed to get_env_files()")
env_files = options.get('env_file', [])
if not isinstance(env_files, list):
env_files = [env_files]
return [expand_path(working_dir, path) for path in env_files]
def resolve_environment(service_dict, working_dir=None):
service_dict = service_dict.copy()
if 'environment' not in service_dict and 'env_file' not in service_dict:
return service_dict
env = {}
if 'env_file' in service_dict:
for f in get_env_files(service_dict, working_dir=working_dir):
env.update(env_vars_from_file(f))
del service_dict['env_file']
env.update(parse_environment(service_dict.get('environment')))
env = dict(resolve_env_var(k, v) for k, v in six.iteritems(env))
service_dict['environment'] = env
return service_dict
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_var(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.
"""
if not os.path.exists(filename):
raise ConfigurationError("Couldn't find env file: %s" % filename)
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
def expand_path(working_dir, path):
return os.path.abspath(os.path.join(working_dir, path))
class ConfigurationError(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg

View File

@ -3,6 +3,7 @@ from __future__ import absolute_import
import logging import logging
from functools import reduce from functools import reduce
from .config import ConfigurationError
from .service import Service from .service import Service
from .container import Container from .container import Container
from docker.errors import APIError from docker.errors import APIError
@ -85,16 +86,6 @@ class Project(object):
volumes_from=volumes_from, **service_dict)) volumes_from=volumes_from, **service_dict))
return project 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): def get_service(self, name):
""" """
Retrieve a service by name. Raises NoSuchService Retrieve a service by name. Raises NoSuchService
@ -277,13 +268,5 @@ class NoSuchService(Exception):
return self.msg return self.msg
class ConfigurationError(Exception):
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg
class DependencyError(ConfigurationError): class DependencyError(ConfigurationError):
pass pass

View File

@ -8,51 +8,14 @@ from operator import attrgetter
import sys import sys
from docker.errors import APIError from docker.errors import APIError
import six
from .config import DOCKER_CONFIG_KEYS
from .container import Container, get_container_name from .container import Container, get_container_name
from .progress_stream import stream_output, StreamOutputError from .progress_stream import stream_output, StreamOutputError
log = logging.getLogger(__name__) 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 = [ DOCKER_START_KEYS = [
'cap_add', 'cap_add',
'cap_drop', 'cap_drop',
@ -96,20 +59,6 @@ class Service(object):
if 'image' in options and 'build' in options: 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) 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.name = name
self.client = client self.client = client
self.project = project self.project = project
@ -478,8 +427,6 @@ class Service(object):
(parse_volume_spec(v).internal, {}) (parse_volume_spec(v).internal, {})
for v in container_options['volumes']) for v in container_options['volumes'])
container_options['environment'] = build_environment(container_options)
if self.can_be_built(): if self.can_be_built():
container_options['image'] = self.full_name container_options['image'] = self.full_name
else: else:
@ -648,63 +595,3 @@ def split_port(port):
external_ip, external_port, internal_port = parts external_ip, external_port, internal_port = parts
return internal_port, (external_ip, external_port or None) 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

View File

@ -158,11 +158,18 @@ environment:
Add environment variables from a file. Can be a single value or a list. Add environment variables from a file. Can be a single value or a list.
If you have specified a Compose file with `docker-compose -f FILE`, paths in
`env_file` are relative to the directory that file is in.
Environment variables specified in `environment` override these values. Environment variables specified in `environment` override these values.
``` ```
env_file: .env
env_file: env_file:
- .env - ./common.env
- ./apps/web.env
- /opt/secrets.env
``` ```
``` ```

View File

@ -0,0 +1,4 @@
web:
image: busybox
command: /bin/true
env_file: ./test.env

1
tests/fixtures/env-file/test.env vendored Normal file
View File

@ -0,0 +1 @@
FOO=1

View File

@ -1,5 +1,6 @@
from __future__ import absolute_import from __future__ import absolute_import
import sys import sys
import os
from six import StringIO from six import StringIO
from mock import patch from mock import patch
@ -23,6 +24,12 @@ class CLITestCase(DockerClientTestCase):
@property @property
def project(self): def project(self):
# Hack: allow project to be overridden. This needs refactoring so that
# the project object is built exactly once, by the command object, and
# accessed by the test case object.
if hasattr(self, '_project'):
return self._project
return self.command.get_project(self.command.get_config_path()) return self.command.get_project(self.command.get_config_path())
def test_help(self): def test_help(self):
@ -409,3 +416,12 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(get_port(3000), container.get_local_port(3000)) self.assertEqual(get_port(3000), container.get_local_port(3000))
self.assertEqual(get_port(3001), "0.0.0.0:9999") self.assertEqual(get_port(3001), "0.0.0.0:9999")
self.assertEqual(get_port(3002), "") self.assertEqual(get_port(3002), "")
def test_env_file_relative_to_compose_file(self):
config_path = os.path.abspath('tests/fixtures/env-file/docker-compose.yml')
self.command.dispatch(['-f', config_path, 'up', '-d'], None)
self._project = self.command.get_project(config_path)
containers = self.project.containers(stopped=True)
self.assertEqual(len(containers), 1)
self.assertIn("FOO=1", containers[0].get('Config.Env'))

View File

@ -1,14 +1,15 @@
from __future__ import unicode_literals 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 compose.container import Container
from .testcases import DockerClientTestCase from .testcases import DockerClientTestCase
class ProjectTest(DockerClientTestCase): class ProjectTest(DockerClientTestCase):
def test_volumes_from_service(self): def test_volumes_from_service(self):
project = Project.from_config( project = Project.from_dicts(
name='composetest', name='composetest',
config={ service_dicts=config.from_dictionary({
'data': { 'data': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'volumes': ['/var/data'], 'volumes': ['/var/data'],
@ -17,7 +18,7 @@ class ProjectTest(DockerClientTestCase):
'image': 'busybox:latest', 'image': 'busybox:latest',
'volumes_from': ['data'], 'volumes_from': ['data'],
}, },
}, }),
client=self.client, client=self.client,
) )
db = project.get_service('db') db = project.get_service('db')
@ -31,14 +32,14 @@ class ProjectTest(DockerClientTestCase):
volumes=['/var/data'], volumes=['/var/data'],
name='composetest_data_container', name='composetest_data_container',
) )
project = Project.from_config( project = Project.from_dicts(
name='composetest', name='composetest',
config={ service_dicts=config.from_dictionary({
'db': { 'db': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'volumes_from': ['composetest_data_container'], 'volumes_from': ['composetest_data_container'],
}, },
}, }),
client=self.client, client=self.client,
) )
db = project.get_service('db') db = project.get_service('db')
@ -48,9 +49,9 @@ class ProjectTest(DockerClientTestCase):
project.remove_stopped() project.remove_stopped()
def test_net_from_service(self): def test_net_from_service(self):
project = Project.from_config( project = Project.from_dicts(
name='composetest', name='composetest',
config={ service_dicts=config.from_dictionary({
'net': { 'net': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': ["/bin/sleep", "300"] 'command': ["/bin/sleep", "300"]
@ -59,8 +60,8 @@ class ProjectTest(DockerClientTestCase):
'image': 'busybox:latest', 'image': 'busybox:latest',
'net': 'container:net', 'net': 'container:net',
'command': ["/bin/sleep", "300"] 'command': ["/bin/sleep", "300"]
}, },
}, }),
client=self.client, client=self.client,
) )
@ -82,14 +83,14 @@ class ProjectTest(DockerClientTestCase):
) )
net_container.start() net_container.start()
project = Project.from_config( project = Project.from_dicts(
name='composetest', name='composetest',
config={ service_dicts=config.from_dictionary({
'web': { 'web': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'net': 'container:composetest_net_container' 'net': 'container:composetest_net_container'
}, },
}, }),
client=self.client, client=self.client,
) )
@ -257,9 +258,9 @@ class ProjectTest(DockerClientTestCase):
project.remove_stopped() project.remove_stopped()
def test_project_up_starts_depends(self): def test_project_up_starts_depends(self):
project = Project.from_config( project = Project.from_dicts(
name='composetest', name='composetest',
config={ service_dicts=config.from_dictionary({
'console': { 'console': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': ["/bin/sleep", "300"], 'command': ["/bin/sleep", "300"],
@ -278,7 +279,7 @@ class ProjectTest(DockerClientTestCase):
'command': ["/bin/sleep", "300"], 'command': ["/bin/sleep", "300"],
'links': ['db'], 'links': ['db'],
}, },
}, }),
client=self.client, client=self.client,
) )
project.start() project.start()
@ -295,9 +296,9 @@ class ProjectTest(DockerClientTestCase):
project.remove_stopped() project.remove_stopped()
def test_project_up_with_no_deps(self): def test_project_up_with_no_deps(self):
project = Project.from_config( project = Project.from_dicts(
name='composetest', name='composetest',
config={ service_dicts=config.from_dictionary({
'console': { 'console': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': ["/bin/sleep", "300"], 'command': ["/bin/sleep", "300"],
@ -316,7 +317,7 @@ class ProjectTest(DockerClientTestCase):
'command': ["/bin/sleep", "300"], 'command': ["/bin/sleep", "300"],
'links': ['db'], 'links': ['db'],
}, },
}, }),
client=self.client, client=self.client,
) )
project.start() project.start()

View File

@ -2,6 +2,7 @@ from __future__ import unicode_literals
from __future__ import absolute_import from __future__ import absolute_import
import os import os
from os import path from os import path
import mock
from compose import Service from compose import Service
from compose.service import CannotBeScaledError 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(): for k,v in {'ONE': '1', 'TWO': '2', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'}.items():
self.assertEqual(env[k], v) self.assertEqual(env[k], v)
@mock.patch.dict(os.environ)
def test_resolve_env(self): 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'] = 'E1'
os.environ['FILE_DEF_EMPTY'] = 'E2' os.environ['FILE_DEF_EMPTY'] = 'E2'
os.environ['ENV_DEF'] = 'E3' os.environ['ENV_DEF'] = 'E3'
try: 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 env = create_and_start_container(service).environment
for k,v in {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}.items(): for k,v in {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}.items():
self.assertEqual(env[k], v) self.assertEqual(env[k], v)
finally:
del os.environ['FILE_DEF']
del os.environ['FILE_DEF_EMPTY']
del os.environ['ENV_DEF']

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from __future__ import absolute_import from __future__ import absolute_import
from compose.service import Service from compose.service import Service
from compose.config import make_service_dict
from compose.cli.docker_client import docker_client from compose.cli.docker_client import docker_client
from compose.progress_stream import stream_output from compose.progress_stream import stream_output
from .. import unittest from .. import unittest
@ -21,14 +22,15 @@ class DockerClientTestCase(unittest.TestCase):
self.client.remove_image(i) self.client.remove_image(i)
def create_service(self, name, **kwargs): def create_service(self, name, **kwargs):
kwargs['image'] = "busybox:latest"
if 'command' not in kwargs: if 'command' not in kwargs:
kwargs['command'] = ["/bin/sleep", "300"] kwargs['command'] = ["/bin/sleep", "300"]
return Service( return Service(
project='composetest', project='composetest',
name=name,
client=self.client, client=self.client,
image="busybox:latest", **make_service_dict(name, kwargs, working_dir='.')
**kwargs
) )
def check_build(self, *args, **kwargs): def check_build(self, *args, **kwargs):

132
tests/unit/config_test.py Normal file
View File

@ -0,0 +1,132 @@
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': 'one.env'},
'tests/fixtures/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': ['one.env', 'two.env']},
'tests/fixtures/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': 'nonexistent.env'}
self.assertRaises(
config.ConfigurationError,
lambda: config.make_service_dict('foo', options, 'tests/fixtures/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_dict = config.make_service_dict(
'foo',
{'env_file': 'resolve.env'},
'tests/fixtures/env',
)
self.assertEqual(
service_dict['environment'],
{'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''},
)

View File

@ -1,8 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from .. import unittest from .. import unittest
from compose.service import Service from compose.service import Service
from compose.project import Project, ConfigurationError from compose.project import Project
from compose.container import Container from compose.container import Container
from compose import config
import mock import mock
import docker import docker
@ -49,26 +50,21 @@ class ProjectTest(unittest.TestCase):
self.assertEqual(project.services[2].name, 'web') self.assertEqual(project.services[2].name, 'web')
def test_from_config(self): def test_from_config(self):
project = Project.from_config('composetest', { dicts = config.from_dictionary({
'web': { 'web': {
'image': 'busybox:latest', 'image': 'busybox:latest',
}, },
'db': { 'db': {
'image': 'busybox:latest', 'image': 'busybox:latest',
}, },
}, None) })
project = Project.from_dicts('composetest', dicts, None)
self.assertEqual(len(project.services), 2) self.assertEqual(len(project.services), 2)
self.assertEqual(project.get_service('web').name, 'web') self.assertEqual(project.get_service('web').name, 'web')
self.assertEqual(project.get_service('web').options['image'], 'busybox:latest') self.assertEqual(project.get_service('web').options['image'], 'busybox:latest')
self.assertEqual(project.get_service('db').name, 'db') self.assertEqual(project.get_service('db').name, 'db')
self.assertEqual(project.get_service('db').options['image'], 'busybox:latest') 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): def test_get_service(self):
web = Service( web = Service(
project='composetest', project='composetest',

View File

@ -16,7 +16,6 @@ from compose.service import (
build_port_bindings, build_port_bindings,
build_volume_binding, build_volume_binding,
get_container_name, get_container_name,
parse_environment,
parse_repository_tag, parse_repository_tag,
parse_volume_spec, parse_volume_spec,
split_port, split_port,
@ -47,10 +46,6 @@ class ServiceTest(unittest.TestCase):
self.assertRaises(ConfigError, lambda: Service(name='foo', project='_')) self.assertRaises(ConfigError, lambda: Service(name='foo', project='_'))
Service(name='foo', project='bar') 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): def test_get_container_name(self):
self.assertIsNone(get_container_name({})) self.assertIsNone(get_container_name({}))
self.assertEqual(get_container_name({'Name': 'myproject_db_1'}), 'myproject_db_1') self.assertEqual(get_container_name({'Name': 'myproject_db_1'}), 'myproject_db_1')
@ -321,98 +316,3 @@ class ServiceVolumesTest(unittest.TestCase):
binding, binding,
('/home/user', dict(bind='/home/user', ro=False))) ('/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': ''}
)