ソースを参照

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

Fix env file resolution
Ben Firshman 10 年 前
コミット
dae451019b

+ 3 - 10
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))

+ 2 - 1
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

+ 198 - 0
compose/config.py

@@ -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

+ 1 - 18
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

+ 1 - 114
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

+ 8 - 1
docs/yml.md

@@ -158,11 +158,18 @@ environment:
 
 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.
 
 ```
+env_file: .env
+
 env_file:
-  - .env
+  - ./common.env
+  - ./apps/web.env
+  - /opt/secrets.env
 ```
 
 ```

+ 4 - 0
tests/fixtures/env-file/docker-compose.yml

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

+ 1 - 0
tests/fixtures/env-file/test.env

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

+ 16 - 0
tests/integration/cli_test.py

@@ -1,5 +1,6 @@
 from __future__ import absolute_import
 import sys
+import os
 
 from six import StringIO
 from mock import patch
@@ -23,6 +24,12 @@ class CLITestCase(DockerClientTestCase):
 
     @property
     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())
 
     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(3001), "0.0.0.0:9999")
         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'))

+ 21 - 20
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"],
@@ -278,7 +279,7 @@ class ProjectTest(DockerClientTestCase):
                     'command': ["/bin/sleep", "300"],
                     'links': ['db'],
                 },
-            },
+            }),
             client=self.client,
         )
         project.start()
@@ -295,9 +296,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"],
@@ -316,7 +317,7 @@ class ProjectTest(DockerClientTestCase):
                     'command': ["/bin/sleep", "300"],
                     'links': ['db'],
                 },
-            },
+            }),
             client=self.client,
         )
         project.start()

+ 6 - 9
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)

+ 5 - 3
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, working_dir='.')
         )
 
     def check_build(self, *args, **kwargs):

+ 132 - 0
tests/unit/config_test.py

@@ -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': ''},
+        )

+ 5 - 9
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',

+ 0 - 100
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': ''}
-            )