From bf96b3348b660a1e2ed964b7000f706ae661ed43 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 31 May 2016 18:43:21 -0400 Subject: [PATCH 1/3] provider/docker: Add docker-exec command This adds a new core command, `docker-exec`, which allows the user to exec into an already-running container. - Fixes #6566 - Fixes #5193 - Fixes #4904 - Fixes #4057 - Fixes #4179 - Fixes #4903 --- contrib/bash/completion.sh | 2 +- plugins/providers/docker/command/exec.rb | 93 +++++++++++++++++++ plugins/providers/docker/errors.rb | 4 + plugins/providers/docker/plugin.rb | 6 ++ templates/locales/providers_docker.yml | 8 ++ .../providers/docker/command/exec_test.rb | 44 +++++++++ website/source/docs/cli/non-primary.html.md | 1 + website/source/docs/docker/commands.html.md | 77 +++++++++++++++ 8 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 plugins/providers/docker/command/exec.rb create mode 100644 test/unit/plugins/providers/docker/command/exec_test.rb diff --git a/contrib/bash/completion.sh b/contrib/bash/completion.sh index 6ba8c3fa3..f0428e7fc 100644 --- a/contrib/bash/completion.sh +++ b/contrib/bash/completion.sh @@ -53,7 +53,7 @@ __vagrantinvestigate() { _vagrant() { cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - commands="box connect destroy docker-logs docker-run global-status halt help init list-commands login package plugin provision push rdp reload resume rsync rsync-auto share snapshot ssh ssh-config status suspend up version" + commands="box connect destroy docker-exec docker-logs docker-run global-status halt help init list-commands login package plugin provision push rdp reload resume rsync rsync-auto share snapshot ssh ssh-config status suspend up version" if [ $COMP_CWORD == 1 ] then diff --git a/plugins/providers/docker/command/exec.rb b/plugins/providers/docker/command/exec.rb new file mode 100644 index 000000000..ab40fbda7 --- /dev/null +++ b/plugins/providers/docker/command/exec.rb @@ -0,0 +1,93 @@ +module VagrantPlugins + module DockerProvider + module Command + class Exec < Vagrant.plugin("2", :command) + def self.synopsis + "attach to an already-running docker container" + end + + def execute + options = {} + options[:detach] = false + options[:pty] = false + options[:prefix] = true + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant docker-exec [options] [name] -- [args]" + o.separator "" + o.separator "Options:" + o.separator "" + + o.on("--[no-]detach", "Run in the background") do |d| + options[:detach] = d + end + + o.on("-t", "--[no-]tty", "Allocate a pty") do |t| + options[:pty] = t + end + + o.on("--[no-]prefix", "Prefix output with machine names") do |p| + options[:prefix] = p + end + end + + # Parse out the extra args to send to SSH, which is everything + # after the "--" + command = nil + split_index = @argv.index("--") + if split_index + command = @argv.drop(split_index + 1) + @argv = @argv.take(split_index) + end + + # Parse the options + argv = parse_options(opts) + return if !argv + + # Show the error if we don't have "--" _after_ parse_options + # so that "-h" and "--help" work properly. + if !split_index + raise Errors::ExecCommandRequired + end + + target_opts = { provider: :docker } + target_opts[:single_target] = options[:pty] + + with_target_vms(argv, target_opts) do |machine| + if machine.state.id != :running + @env.ui.info("#{machine.id} is not running.") + next + end + exec_command(machine, options, command) + end + + return 0 + end + + def exec_command(machine, options, command) + exec_cmd = %w(docker exec) + exec_cmd << "-it" if options[:pty] + exec_cmd << machine.id + exec_cmd += options[:extra_args] if options[:extra_args] + exec_cmd += command + + # Run this interactively if asked. + exec_options = options + exec_options[:stdin] = true if options[:pty] + + output = "" + machine.provider.driver.execute(*exec_cmd, exec_options) do |type, data| + output += data + end + + output_options = {} + output_options[:prefix] = false if !options[:prefix] + + if !output.empty? + machine.ui.output(output.chomp, **output_options) + end + end + end + end + end +end diff --git a/plugins/providers/docker/errors.rb b/plugins/providers/docker/errors.rb index 4b3e71d64..cdb0afbd7 100644 --- a/plugins/providers/docker/errors.rb +++ b/plugins/providers/docker/errors.rb @@ -13,6 +13,10 @@ module VagrantPlugins error_key(:execute_error) end + class ExecCommandRequired < DockerError + error_key(:exec_command_required) + end + class HostVMCommunicatorNotReady < DockerError error_key(:host_vm_communicator_not_ready) end diff --git a/plugins/providers/docker/plugin.rb b/plugins/providers/docker/plugin.rb index 36ab08687..d5f7c7bd4 100644 --- a/plugins/providers/docker/plugin.rb +++ b/plugins/providers/docker/plugin.rb @@ -22,6 +22,12 @@ module VagrantPlugins Provider end + command("docker-exec", primary: false) do + require_relative "command/exec" + init! + Command::Exec + end + command("docker-logs", primary: false) do require_relative "command/logs" init! diff --git a/templates/locales/providers_docker.yml b/templates/locales/providers_docker.yml index a8dfbced9..2990d50ce 100644 --- a/templates/locales/providers_docker.yml +++ b/templates/locales/providers_docker.yml @@ -153,6 +153,14 @@ en: Stderr: %{stderr} Stdout: %{stdout} + exec_command_required: |- + The "docker-exec" command requires a command to execute. This command + must be specified after a "--" in the command line. This is used to + separate machine name and options from the actual command to execute. + An example is show below: + + $ vagrant docker-exec -t nginx -- bash + host_vm_communicator_not_ready: |- The Docker provider was able to bring up the host VM successfully but the host VM is still reporting that SSH is unavailable. This diff --git a/test/unit/plugins/providers/docker/command/exec_test.rb b/test/unit/plugins/providers/docker/command/exec_test.rb new file mode 100644 index 000000000..6cf94c3b0 --- /dev/null +++ b/test/unit/plugins/providers/docker/command/exec_test.rb @@ -0,0 +1,44 @@ +require_relative "../../../../base" +require_relative "../../../../../../plugins/providers/docker/command/exec" + +describe VagrantPlugins::DockerProvider::Command::Exec do + include_context "unit" + include_context "command plugin helpers" + + let(:sandbox) do + isolated_environment + end + + let(:argv) { [] } + let(:env) { sandbox.create_vagrant_env } + + let(:vagrantfile_path) { File.join(env.cwd, "Vagrantfile") } + + subject { described_class.new(argv, env) } + + before(:all) do + I18n.load_path << Vagrant.source_root.join("templates/locales/providers_docker.yml") + I18n.reload! + end + + before do + Vagrant.plugin("2").manager.stub(commands: {}) + allow(subject).to receive(:exec_command) + end + + after do + sandbox.close + end + + describe "#execute" do + describe "without a command" do + let(:argv) { [] } + + it "raises an error" do + expect { + subject.execute + }.to raise_error(VagrantPlugins::DockerProvider::Errors::ExecCommandRequired) + end + end + end +end diff --git a/website/source/docs/cli/non-primary.html.md b/website/source/docs/cli/non-primary.html.md index 65f96c956..6fc64c1c9 100644 --- a/website/source/docs/cli/non-primary.html.md +++ b/website/source/docs/cli/non-primary.html.md @@ -27,6 +27,7 @@ non-primary subcommands. They're executed just like any other subcommand: The list of non-primary commands is below. Click on any command to learn more about it. +* [docker-exec](/docs/docker/commands.html) * [docker-logs](/docs/docker/commands.html) * [docker-run](/docs/docker/commands.html) * [rsync](/docs/cli/rsync.html) diff --git a/website/source/docs/docker/commands.html.md b/website/source/docs/docker/commands.html.md index e942c5877..ac567df55 100644 --- a/website/source/docs/docker/commands.html.md +++ b/website/source/docs/docker/commands.html.md @@ -16,6 +16,83 @@ useful for interacting with Docker containers. This helps with your workflow on top of Vagrant so that you have full access to Docker underneath. +### docker-exec + +`vagrant docker-exec` can be used to run one-off commands against +a Docker container that is currently running. If the container is not running, +an error will be returned. + +```sh +$ vagrant docker-exec app -- rake db:migrate +``` + +The above would run `rake db:migrate` in the context of an `app` container. + +Note that the "name" corresponds to the name of the VM, **not** the name of the +Docker container. Consider the following Vagrantfile: + +```ruby +Vagrant.configure(2) do |config| + config.vm.provider "docker" do |d| + d.image = "consul" + end +end +``` + +This Vagrantfile will start the official Docker Consul image. However, the +associated Vagrant command to `docker-exec` into this instance is: + +```sh +$ vagrant docker-exec -t -- /bin/sh +``` + +In particular, the command is actually: + +```sh +$ vagrant docker-exec default -t -- /bin/sh +``` + +Because "default" is the default name of the first defined VM. In a +multi-machine Vagrant setup as shown below, the "name" attribute corresponds +to the name of the VM, **not** the name of the container: + +```ruby +Vagrant.configure do |config| + config.vm.define "web" do + config.vm.provider "docker" do |d| + d.image = "nginx" + end + end + + config.vm.define "consul" do + config.vm.provider "docker" do |d| + d.image = "consul" + end + end +end +``` + +The following command is invalid: + +```sh +# Not valid +$ vagrant docker-exec -t nginx -- /bin/sh +``` + +This is because the "name" of the VM is "web", so the command is actually: + +```sh +$ vagrant docker-exec -t web -- /bin/sh +``` + +For this reason, it is recommended that you name the VM the same as the +container. In the above example, it is unambiguous that the command to enter +the Consul container is: + +```sh +$ vagrant docker-exec -t consul -- /bin/sh +``` + ### docker-logs `vagrant docker-logs` can be used to see the logs of a running container. From cfac24779c313d188c3e36f91839802f8145f7dd Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 31 May 2016 20:04:08 -0400 Subject: [PATCH 2/3] provider/docker: Separate -i and -t flags for exec --- plugins/providers/docker/command/exec.rb | 13 ++++++++++--- website/source/docs/docker/commands.html.md | 10 +++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/plugins/providers/docker/command/exec.rb b/plugins/providers/docker/command/exec.rb index ab40fbda7..4172a668d 100644 --- a/plugins/providers/docker/command/exec.rb +++ b/plugins/providers/docker/command/exec.rb @@ -10,6 +10,7 @@ module VagrantPlugins options = {} options[:detach] = false options[:pty] = false + options[:interactive] = false options[:prefix] = true opts = OptionParser.new do |o| @@ -22,6 +23,10 @@ module VagrantPlugins options[:detach] = d end + o.on("-i", "--[no-]interactive", "Keep STDIN open even if not attached") do |i| + options[:interactive] = i + end + o.on("-t", "--[no-]tty", "Allocate a pty") do |t| options[:pty] = t end @@ -58,15 +63,17 @@ module VagrantPlugins @env.ui.info("#{machine.id} is not running.") next end - exec_command(machine, options, command) + exec_command(machine, command, options) end return 0 end - def exec_command(machine, options, command) + def exec_command(machine, command, options) exec_cmd = %w(docker exec) - exec_cmd << "-it" if options[:pty] + exec_cmd << "-i" if options[:interactive] + exec_cmd << "-t" if options[:pty] + exec_cmd << "-u" << options[:user] if options[:user] exec_cmd << machine.id exec_cmd += options[:extra_args] if options[:extra_args] exec_cmd += command diff --git a/website/source/docs/docker/commands.html.md b/website/source/docs/docker/commands.html.md index ac567df55..d282e4281 100644 --- a/website/source/docs/docker/commands.html.md +++ b/website/source/docs/docker/commands.html.md @@ -43,13 +43,13 @@ This Vagrantfile will start the official Docker Consul image. However, the associated Vagrant command to `docker-exec` into this instance is: ```sh -$ vagrant docker-exec -t -- /bin/sh +$ vagrant docker-exec -it -- /bin/sh ``` In particular, the command is actually: ```sh -$ vagrant docker-exec default -t -- /bin/sh +$ vagrant docker-exec default -it -- /bin/sh ``` Because "default" is the default name of the first defined VM. In a @@ -76,13 +76,13 @@ The following command is invalid: ```sh # Not valid -$ vagrant docker-exec -t nginx -- /bin/sh +$ vagrant docker-exec -it nginx -- /bin/sh ``` This is because the "name" of the VM is "web", so the command is actually: ```sh -$ vagrant docker-exec -t web -- /bin/sh +$ vagrant docker-exec -it web -- /bin/sh ``` For this reason, it is recommended that you name the VM the same as the @@ -90,7 +90,7 @@ container. In the above example, it is unambiguous that the command to enter the Consul container is: ```sh -$ vagrant docker-exec -t consul -- /bin/sh +$ vagrant docker-exec -it consul -- /bin/sh ``` ### docker-logs From e270e7df275278830a400958258349a75842303e Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 31 May 2016 20:04:27 -0400 Subject: [PATCH 3/3] provider/docker: Add -u flag to exec --- plugins/providers/docker/command/exec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/providers/docker/command/exec.rb b/plugins/providers/docker/command/exec.rb index 4172a668d..daf5f8e98 100644 --- a/plugins/providers/docker/command/exec.rb +++ b/plugins/providers/docker/command/exec.rb @@ -31,6 +31,10 @@ module VagrantPlugins options[:pty] = t end + o.on("-u", "--user USER", "User or UID") do |u| + options[:user] = u + end + o.on("--[no-]prefix", "Prefix output with machine names") do |p| options[:prefix] = p end