diff --git a/compose/cli/command.py b/compose/cli/command.py index c1f6c2925..f9e698e17 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -39,7 +39,8 @@ SILENT_COMMANDS = set(( def project_from_options(project_dir, options): override_dir = options.get('--project-directory') - environment = Environment.from_env_file(override_dir or project_dir) + environment_file = options.get('--env-file') + environment = Environment.from_env_file(override_dir or project_dir, environment_file) environment.silent = options.get('COMMAND', None) in SILENT_COMMANDS set_parallel_limit(environment) @@ -77,7 +78,8 @@ def set_parallel_limit(environment): def get_config_from_options(base_dir, options): override_dir = options.get('--project-directory') - environment = Environment.from_env_file(override_dir or base_dir) + environment_file = options.get('--env-file') + environment = Environment.from_env_file(override_dir or base_dir, environment_file) config_path = get_config_path_from_options( base_dir, options, environment ) diff --git a/compose/cli/main.py b/compose/cli/main.py index 08976347e..d8793c85b 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -208,6 +208,7 @@ class TopLevelCommand(object): (default: the path of the Compose file) --compatibility If set, Compose will attempt to convert keys in v3 files to their non-Swarm equivalent + --env-file PATH Specify an alternate environment file Commands: build Build or rebuild services @@ -274,7 +275,8 @@ class TopLevelCommand(object): '--build-arg is only supported when services are specified for API version < 1.25.' ' Please use a Compose file version > 2.2 or specify which services to build.' ) - environment = Environment.from_env_file(self.project_dir) + environment_file = options.get('--env-file') + environment = Environment.from_env_file(self.project_dir, environment_file) build_args = resolve_build_args(build_args, environment) self.project.build( @@ -423,8 +425,10 @@ class TopLevelCommand(object): Compose file -t, --timeout TIMEOUT Specify a shutdown timeout in seconds. (default: 10) + --env-file PATH Specify an alternate environment file """ - environment = Environment.from_env_file(self.project_dir) + environment_file = options.get('--env-file') + environment = Environment.from_env_file(self.project_dir, environment_file) ignore_orphans = environment.get_boolean('COMPOSE_IGNORE_ORPHANS') if ignore_orphans and options['--remove-orphans']: @@ -481,8 +485,10 @@ class TopLevelCommand(object): -e, --env KEY=VAL Set environment variables (can be used multiple times, not supported in API < 1.25) -w, --workdir DIR Path to workdir directory for this command. + --env-file PATH Specify an alternate environment file """ - environment = Environment.from_env_file(self.project_dir) + environment_file = options.get('--env-file') + environment = Environment.from_env_file(self.project_dir, environment_file) use_cli = not environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI') index = int(options.get('--index')) service = self.project.get_service(options['SERVICE']) @@ -1038,6 +1044,7 @@ class TopLevelCommand(object): container. Implies --abort-on-container-exit. --scale SERVICE=NUM Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present. + --env-file PATH Specify an alternate environment file """ start_deps = not options['--no-deps'] always_recreate_deps = options['--always-recreate-deps'] @@ -1052,7 +1059,8 @@ class TopLevelCommand(object): if detached and (cascade_stop or exit_value_from): raise UserError("--abort-on-container-exit and -d cannot be combined.") - environment = Environment.from_env_file(self.project_dir) + environment_file = options.get('--env-file') + environment = Environment.from_env_file(self.project_dir, environment_file) ignore_orphans = environment.get_boolean('COMPOSE_IGNORE_ORPHANS') if ignore_orphans and remove_orphans: @@ -1345,7 +1353,8 @@ def run_one_off_container(container_options, project, service, options, toplevel if options['--rm']: project.client.remove_container(container.id, force=True, v=True) - environment = Environment.from_env_file(project_dir) + environment_file = options.get('--env-file') + environment = Environment.from_env_file(project_dir, environment_file) use_cli = not environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI') signals.set_signal_handler_to_shutdown() diff --git a/compose/config/environment.py b/compose/config/environment.py index 62c40a4bc..e2db343d7 100644 --- a/compose/config/environment.py +++ b/compose/config/environment.py @@ -59,12 +59,15 @@ class Environment(dict): self.silent = False @classmethod - def from_env_file(cls, base_dir): + def from_env_file(cls, base_dir, env_file=None): def _initialize(): result = cls() if base_dir is None: return result - env_file_path = os.path.join(base_dir, '.env') + if env_file: + env_file_path = os.path.join(base_dir, env_file) + else: + env_file_path = os.path.join(base_dir, '.env') try: return cls(env_vars_from_file(env_file_path)) except EnvFileNotFound: diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 8a6415b40..6a9a392a5 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -324,6 +324,21 @@ class CLITestCase(DockerClientTestCase): 'version': '2.4' } + def test_config_with_env_file(self): + self.base_dir = 'tests/fixtures/default-env-file' + result = self.dispatch(['--env-file', '.env2', 'config']) + json_result = yaml.load(result.stdout) + assert json_result == { + 'services': { + 'web': { + 'command': 'false', + 'image': 'alpine:latest', + 'ports': ['5644/tcp', '9998/tcp'] + } + }, + 'version': '2.4' + } + def test_config_with_dot_env_and_override_dir(self): self.base_dir = 'tests/fixtures/default-env-file' result = self.dispatch(['--project-directory', 'alt/', 'config']) diff --git a/tests/fixtures/default-env-file/.env2 b/tests/fixtures/default-env-file/.env2 new file mode 100644 index 000000000..d754523fc --- /dev/null +++ b/tests/fixtures/default-env-file/.env2 @@ -0,0 +1,4 @@ +IMAGE=alpine:latest +COMMAND=false +PORT1=5644 +PORT2=9998 diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index c2e6b7b07..888c0ae58 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -3465,6 +3465,25 @@ class InterpolationTest(unittest.TestCase): 'command': 'true' } + @mock.patch.dict(os.environ) + def test_config_file_with_options_environment_file(self): + project_dir = 'tests/fixtures/default-env-file' + service_dicts = config.load( + config.find( + project_dir, None, Environment.from_env_file(project_dir, '.env2') + ) + ).services + + assert service_dicts[0] == { + 'name': 'web', + 'image': 'alpine:latest', + 'ports': [ + types.ServicePort.parse('5644')[0], + types.ServicePort.parse('9998')[0] + ], + 'command': 'false' + } + @mock.patch.dict(os.environ) def test_config_file_with_environment_variable(self): project_dir = 'tests/fixtures/environment-interpolation'