Skip to content

Commit

Permalink
provider/docker: Add docker-exec command
Browse files Browse the repository at this point in the history
This adds a new core command, `docker-exec`, which allows the user to
exec into an already-running container.

- Fixes hashicorp#6566
- Fixes hashicorp#5193
- Fixes hashicorp#4904
- Fixes hashicorp#4057
- Fixes hashicorp#4179
- Fixes hashicorp#4903
  • Loading branch information
sethvargo committed Jun 1, 2016
1 parent 084a8e8 commit bf96b33
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 1 deletion.
2 changes: 1 addition & 1 deletion contrib/bash/completion.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 93 additions & 0 deletions plugins/providers/docker/command/exec.rb
Original file line number Diff line number Diff line change
@@ -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] -- <command> [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
4 changes: 4 additions & 0 deletions plugins/providers/docker/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class ExecuteError < DockerError
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
Expand Down
6 changes: 6 additions & 0 deletions plugins/providers/docker/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ class Plugin < Vagrant.plugin("2")
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!
Expand Down
8 changes: 8 additions & 0 deletions templates/locales/providers_docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions test/unit/plugins/providers/docker/command/exec_test.rb
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions website/source/docs/cli/non-primary.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
77 changes: 77 additions & 0 deletions website/source/docs/docker/commands.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit bf96b33

Please sign in to comment.