From 99464d9c2bfc9052ca341dc0e57c0ab2760cd8a2 Mon Sep 17 00:00:00 2001 From: Klaas Hoekema Date: Sun, 16 Jun 2019 22:57:04 -0400 Subject: [PATCH 1/5] Handle environment file override within TopLevelCommand Several (but not all) of the subcommands are accepting and processing the `--env-file` option, but only because they need to look for a specific value in the environment. The work of applying the override makes more sense as the domain of TopLevelCommand, and moving it there and removing the option from the subcommands makes things simpler. Signed-off-by: Klaas Hoekema --- compose/cli/main.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index 6391bd3ea2f..e11554ac095 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -247,6 +247,11 @@ def __init__(self, project, options=None): def project_dir(self): return self.toplevel_options.get('--project-directory') or '.' + @property + def environment(self): + environment_file = self.toplevel_options.get('--env-file') + return Environment.from_env_file(self.project_dir, environment_file) + def build(self, options): """ Build or rebuild services. @@ -276,9 +281,7 @@ def build(self, options): '--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_file = options.get('--env-file') - environment = Environment.from_env_file(self.project_dir, environment_file) - build_args = resolve_build_args(build_args, environment) + build_args = resolve_build_args(build_args, self.environment) self.project.build( service_names=options['SERVICE'], @@ -429,11 +432,8 @@ def down(self, options): Compose file -t, --timeout TIMEOUT Specify a shutdown timeout in seconds. (default: 10) - --env-file PATH Specify an alternate environment file """ - environment_file = options.get('--env-file') - environment = Environment.from_env_file(self.project_dir, environment_file) - ignore_orphans = environment.get_boolean('COMPOSE_IGNORE_ORPHANS') + ignore_orphans = self.environment.get_boolean('COMPOSE_IGNORE_ORPHANS') if ignore_orphans and options['--remove-orphans']: raise UserError("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined.") @@ -489,11 +489,8 @@ def exec_command(self, options): -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_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') + use_cli = not self.environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI') index = int(options.get('--index')) service = self.project.get_service(options['SERVICE']) detach = options.get('--detach') @@ -1051,7 +1048,6 @@ def up(self, options): 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'] @@ -1066,9 +1062,7 @@ def up(self, options): if detached and (cascade_stop or exit_value_from): raise UserError("--abort-on-container-exit and -d cannot be combined.") - environment_file = options.get('--env-file') - environment = Environment.from_env_file(self.project_dir, environment_file) - ignore_orphans = environment.get_boolean('COMPOSE_IGNORE_ORPHANS') + ignore_orphans = self.environment.get_boolean('COMPOSE_IGNORE_ORPHANS') if ignore_orphans and remove_orphans: raise UserError("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined.") @@ -1360,7 +1354,7 @@ def remove_container(force=False): if options['--rm']: project.client.remove_container(container.id, force=True, v=True) - environment_file = options.get('--env-file') + environment_file = toplevel_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() From 35eb40424c45209df3c23a33ae9831413abf7f26 Mon Sep 17 00:00:00 2001 From: Klaas Hoekema Date: Thu, 11 Jul 2019 22:28:18 -0400 Subject: [PATCH 2/5] Call TopLevelCommand's environment 'toplevel_environment' To help prevent confusion between the different meanings and sources of "environment", rename the method that loads the environment from the .env or --env-file (i.e. the one that applies at a project level) to 'toplevel_environment'. Signed-off-by: Klaas Hoekema --- compose/cli/main.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index e11554ac095..eb6e7d82082 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -248,7 +248,7 @@ def project_dir(self): return self.toplevel_options.get('--project-directory') or '.' @property - def environment(self): + def toplevel_environment(self): environment_file = self.toplevel_options.get('--env-file') return Environment.from_env_file(self.project_dir, environment_file) @@ -281,7 +281,7 @@ def build(self, options): '--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.' ) - build_args = resolve_build_args(build_args, self.environment) + build_args = resolve_build_args(build_args, self.toplevel_environment) self.project.build( service_names=options['SERVICE'], @@ -433,7 +433,7 @@ def down(self, options): -t, --timeout TIMEOUT Specify a shutdown timeout in seconds. (default: 10) """ - ignore_orphans = self.environment.get_boolean('COMPOSE_IGNORE_ORPHANS') + ignore_orphans = self.toplevel_environment.get_boolean('COMPOSE_IGNORE_ORPHANS') if ignore_orphans and options['--remove-orphans']: raise UserError("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined.") @@ -490,7 +490,7 @@ def exec_command(self, options): not supported in API < 1.25) -w, --workdir DIR Path to workdir directory for this command. """ - use_cli = not self.environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI') + use_cli = not self.toplevel_environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI') index = int(options.get('--index')) service = self.project.get_service(options['SERVICE']) detach = options.get('--detach') @@ -513,7 +513,7 @@ def exec_command(self, options): if IS_WINDOWS_PLATFORM or use_cli and not detach: sys.exit(call_docker( build_exec_command(options, container.id, command), - self.toplevel_options, environment) + self.toplevel_options, self.toplevel_environment) ) create_exec_options = { @@ -1062,7 +1062,7 @@ def up(self, options): if detached and (cascade_stop or exit_value_from): raise UserError("--abort-on-container-exit and -d cannot be combined.") - ignore_orphans = self.environment.get_boolean('COMPOSE_IGNORE_ORPHANS') + ignore_orphans = self.toplevel_environment.get_boolean('COMPOSE_IGNORE_ORPHANS') if ignore_orphans and remove_orphans: raise UserError("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined.") @@ -1355,8 +1355,9 @@ def remove_container(force=False): project.client.remove_container(container.id, force=True, v=True) environment_file = toplevel_options.get('--env-file') - environment = Environment.from_env_file(project_dir, environment_file) - use_cli = not environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI') + toplevel_environment = Environment.from_env_file(project_dir, environment_file) + use_cli = not toplevel_environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI') + signals.set_signal_handler_to_shutdown() signals.set_signal_handler_to_hang_up() try: @@ -1365,7 +1366,7 @@ def remove_container(force=False): service.connect_container_to_networks(container, use_network_aliases) exit_code = call_docker( get_docker_start_call(container_options, container.id), - toplevel_options, environment + toplevel_options, toplevel_environment ) else: operation = RunOperation( From 088a798e7a546755537cb9082b440f17d64d6bdf Mon Sep 17 00:00:00 2001 From: Klaas Hoekema Date: Thu, 11 Jul 2019 23:27:11 -0400 Subject: [PATCH 3/5] Fix typo in 'split_env' error message Signed-off-by: Klaas Hoekema --- compose/config/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/config/environment.py b/compose/config/environment.py index e72c8823129..696356f3246 100644 --- a/compose/config/environment.py +++ b/compose/config/environment.py @@ -26,7 +26,7 @@ def split_env(env): key = env if re.search(r'\s', key): raise ConfigurationError( - "environment variable name '{}' may not contains whitespace.".format(key) + "environment variable name '{}' may not contain whitespace.".format(key) ) return key, value From 69c0683bfe8f4bbb90d07f0db6516e51942f97d6 Mon Sep 17 00:00:00 2001 From: Klaas Hoekema Date: Fri, 12 Jul 2019 12:59:31 -0400 Subject: [PATCH 4/5] Pass toplevel_environment to run_one_off_container Instead of passing `project_dir` from `TopLevelCommand.run` to `run_one_off_container` then using it there to load the toplevel environment (duplicating the logic that `TopLevelCommand.toplevel_environment` encapsulates), pass the Environment object. Signed-off-by: Klaas Hoekema --- compose/cli/main.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index eb6e7d82082..477b57b52ef 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -887,7 +887,7 @@ def run(self, options): container_options = build_one_off_container_options(options, detach, command) run_one_off_container( container_options, self.project, service, options, - self.toplevel_options, self.project_dir + self.toplevel_options, self.toplevel_environment ) def scale(self, options): @@ -1325,7 +1325,7 @@ def build_one_off_container_options(options, detach, command): def run_one_off_container(container_options, project, service, options, toplevel_options, - project_dir='.'): + toplevel_environment): if not options['--no-deps']: deps = service.get_dependency_names() if deps: @@ -1354,8 +1354,6 @@ def remove_container(force=False): if options['--rm']: project.client.remove_container(container.id, force=True, v=True) - environment_file = toplevel_options.get('--env-file') - toplevel_environment = Environment.from_env_file(project_dir, environment_file) use_cli = not toplevel_environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI') signals.set_signal_handler_to_shutdown() From 413e5db7b35684a543e1c14f89aad379167f9b06 Mon Sep 17 00:00:00 2001 From: Klaas Hoekema Date: Mon, 22 Jul 2019 13:59:49 -0400 Subject: [PATCH 5/5] Add shell completions for --env-file option Adds completions for the --env-file toplevel option to the bash, fish, and zsh completions files. Signed-off-by: Klaas Hoekema --- contrib/completion/bash/docker-compose | 5 +++++ contrib/completion/fish/docker-compose.fish | 1 + contrib/completion/zsh/_docker-compose | 2 ++ 3 files changed, 8 insertions(+) diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index e9168b1b16a..6dc47799db5 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -184,6 +184,10 @@ _docker_compose_docker_compose() { _filedir -d return ;; + --env-file) + _filedir + return + ;; $(__docker_compose_to_extglob "$daemon_options_with_args") ) return ;; @@ -612,6 +616,7 @@ _docker_compose() { --tlsverify " local daemon_options_with_args=" + --env-file --file -f --host -H --project-directory diff --git a/contrib/completion/fish/docker-compose.fish b/contrib/completion/fish/docker-compose.fish index 69ecc505685..0566e16ae88 100644 --- a/contrib/completion/fish/docker-compose.fish +++ b/contrib/completion/fish/docker-compose.fish @@ -12,6 +12,7 @@ end complete -c docker-compose -s f -l file -r -d 'Specify an alternate compose file' complete -c docker-compose -s p -l project-name -x -d 'Specify an alternate project name' +complete -c docker-compose -l env-file -r -d 'Specify an alternate environment file (default: .env)' complete -c docker-compose -l verbose -d 'Show more output' complete -c docker-compose -s H -l host -x -d 'Daemon socket to connect to' complete -c docker-compose -l tls -d 'Use TLS; implied by --tlsverify' diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 808b068a314..faf4059888f 100755 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -341,6 +341,7 @@ _docker-compose() { '(- :)'{-h,--help}'[Get help]' \ '*'{-f,--file}"[${file_description}]:file:_files -g '*.yml'" \ '(-p --project-name)'{-p,--project-name}'[Specify an alternate project name (default: directory name)]:project name:' \ + '--env-file[Specify an alternate environment file (default: .env)]:env-file:_files' \ "--compatibility[If set, Compose will attempt to convert keys in v3 files to their non-Swarm equivalent]" \ '(- :)'{-v,--version}'[Print version and exit]' \ '--verbose[Show more output]' \ @@ -359,6 +360,7 @@ _docker-compose() { local -a relevant_compose_flags relevant_compose_repeatable_flags relevant_docker_flags compose_options docker_options relevant_compose_flags=( + "--env-file" "--file" "-f" "--host" "-H" "--project-name" "-p"