Resolves #927 - fix merging command line environment with a list in the config

Signed-off-by: Daniel Nephin <dnephin@gmail.com>
This commit is contained in:
Daniel Nephin 2015-02-14 14:09:55 -05:00
parent 8610adcaf3
commit f47431d591
5 changed files with 103 additions and 45 deletions

View File

@ -1,26 +1,25 @@
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
from inspect import getdoc
from operator import attrgetter
import logging import logging
import sys
import re import re
import signal import signal
from operator import attrgetter import sys
from inspect import getdoc from docker.errors import APIError
import dockerpty import dockerpty
from .. import __version__ from .. import __version__
from ..project import NoSuchService, ConfigurationError from ..project import NoSuchService, ConfigurationError
from ..service import BuildError, CannotBeScaledError from ..service import BuildError, CannotBeScaledError, parse_environment
from .command import Command from .command import Command
from .docopt_command import NoSuchCommand
from .errors import UserError
from .formatter import Formatter from .formatter import Formatter
from .log_printer import LogPrinter from .log_printer import LogPrinter
from .utils import yesno from .utils import yesno
from docker.errors import APIError
from .errors import UserError
from .docopt_command import NoSuchCommand
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -316,11 +315,10 @@ class TopLevelCommand(Command):
} }
if options['-e']: if options['-e']:
for option in options['-e']: # Merge environment from config with -e command line
if 'environment' not in service.options: container_options['environment'] = dict(
service.options['environment'] = {} parse_environment(service.options.get('environment')),
k, v = option.split('=', 1) **parse_environment(options['-e']))
service.options['environment'][k] = v
if options['--entrypoint']: if options['--entrypoint']:
container_options['entrypoint'] = options.get('--entrypoint') container_options['entrypoint'] = options.get('--entrypoint')

View File

@ -8,6 +8,7 @@ from operator import attrgetter
import sys import sys
from docker.errors import APIError from docker.errors import APIError
import six
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
@ -450,7 +451,7 @@ 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'] = merge_environment(container_options) 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
@ -629,19 +630,28 @@ def get_env_files(options):
return env_files return env_files
def merge_environment(options): def build_environment(options):
env = {} env = {}
for f in get_env_files(options): for f in get_env_files(options):
env.update(env_vars_from_file(f)) env.update(env_vars_from_file(f))
if 'environment' in options: env.update(parse_environment(options.get('environment')))
if isinstance(options['environment'], list): return dict(resolve_env(k, v) for k, v in six.iteritems(env))
env.update(dict(split_env(e) for e in options['environment']))
else:
env.update(options['environment'])
return dict(resolve_env(k, v) for k, v in env.items())
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): def split_env(env):

View File

@ -6,12 +6,14 @@ import tempfile
import shutil import shutil
from .. import unittest from .. import unittest
import docker
import mock import mock
from six import StringIO
from compose.cli import main from compose.cli import main
from compose.cli.main import TopLevelCommand from compose.cli.main import TopLevelCommand
from compose.cli.errors import ComposeFileNotFound from compose.cli.errors import ComposeFileNotFound
from six import StringIO from compose.service import Service
class CLITestCase(unittest.TestCase): class CLITestCase(unittest.TestCase):
@ -103,6 +105,35 @@ class CLITestCase(unittest.TestCase):
self.assertEqual(logging.getLogger().level, logging.DEBUG) self.assertEqual(logging.getLogger().level, logging.DEBUG)
self.assertEqual(logging.getLogger('requests').propagate, False) self.assertEqual(logging.getLogger('requests').propagate, False)
@mock.patch('compose.cli.main.dockerpty', autospec=True)
def test_run_with_environment_merged_with_options_list(self, mock_dockerpty):
command = TopLevelCommand()
mock_client = mock.create_autospec(docker.Client)
mock_project = mock.Mock()
mock_project.get_service.return_value = Service(
'service',
client=mock_client,
environment=['FOO=ONE', 'BAR=TWO'],
image='someimage')
command.run(mock_project, {
'SERVICE': 'service',
'COMMAND': None,
'-e': ['BAR=NEW', 'OTHER=THREE'],
'--no-deps': None,
'--allow-insecure-ssl': None,
'-d': True,
'-T': None,
'--entrypoint': None,
'--service-ports': None,
'--rm': None,
})
_, _, call_kwargs = mock_client.create_container.mock_calls[0]
self.assertEqual(
call_kwargs['environment'],
{'FOO': 'ONE', 'BAR': 'NEW', 'OTHER': 'THREE'})
def get_config_filename_for_files(filenames): def get_config_filename_for_files(filenames):
project_dir = tempfile.mkdtemp() project_dir = tempfile.mkdtemp()

View File

@ -11,14 +11,15 @@ from requests import Response
from compose import Service from compose import Service
from compose.container import Container from compose.container import Container
from compose.service import ( from compose.service import (
ConfigError,
split_port,
build_port_bindings,
parse_volume_spec,
build_volume_binding,
APIError, APIError,
ConfigError,
build_port_bindings,
build_volume_binding,
get_container_name, get_container_name,
parse_environment,
parse_repository_tag, parse_repository_tag,
parse_volume_spec,
split_port,
) )
@ -326,28 +327,47 @@ class ServiceEnvironmentTest(unittest.TestCase):
self.mock_client = mock.create_autospec(docker.Client) self.mock_client = mock.create_autospec(docker.Client)
self.mock_client.containers.return_value = [] self.mock_client.containers.return_value = []
def test_parse_environment(self): def test_parse_environment_as_list(self):
service = Service('foo', environment =[
environment=['NORMAL=F1', 'CONTAINS_EQUALS=F=2', 'TRAILING_EQUALS='], 'NORMAL=F1',
client=self.mock_client, 'CONTAINS_EQUALS=F=2',
image='image_name', 'TRAILING_EQUALS='
) ]
options = service._get_container_create_options({})
self.assertEqual( self.assertEqual(
options['environment'], parse_environment(environment),
{'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''} {'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) @mock.patch.dict(os.environ)
def test_resolve_environment(self): def test_resolve_environment(self):
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'
service = Service('foo', service = Service(
environment={'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': None, 'NO_DEF': None}, 'foo',
client=self.mock_client, environment={
image='image_name', '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({}) options = service._get_container_create_options({})
self.assertEqual( self.assertEqual(
options['environment'], options['environment'],
@ -381,7 +401,6 @@ class ServiceEnvironmentTest(unittest.TestCase):
def test_env_nonexistent_file(self): def test_env_nonexistent_file(self):
self.assertRaises(ConfigError, lambda: Service('foo', env_file='tests/fixtures/env/nonexistent.env')) self.assertRaises(ConfigError, lambda: Service('foo', env_file='tests/fixtures/env/nonexistent.env'))
@mock.patch.dict(os.environ) @mock.patch.dict(os.environ)
def test_resolve_environment_from_file(self): def test_resolve_environment_from_file(self):
os.environ['FILE_DEF'] = 'E1' os.environ['FILE_DEF'] = 'E1'

View File

@ -8,9 +8,9 @@ deps =
-rrequirements-dev.txt -rrequirements-dev.txt
commands = commands =
nosetests -v {posargs} nosetests -v {posargs}
flake8 fig flake8 compose
[flake8] [flake8]
# ignore line-length for now # ignore line-length for now
ignore = E501,E203 ignore = E501,E203
exclude = fig/packages exclude = compose/packages