Prior to this commit, if a created but exited container bound a port, and a new container grabed that same port (say for an ssh port forward), when the initial container came back up it would fail because the port also got bound to the second container. This commit fixes that behavior by first looking at what containers are already bound prior to creating a container.
197 lines
6.3 KiB
Ruby
197 lines
6.3 KiB
Ruby
require "digest/md5"
|
|
require "tempfile"
|
|
|
|
module VagrantPlugins
|
|
module DockerProvider
|
|
# This communicator uses the host VM as proxy to communicate to the
|
|
# actual Docker container via SSH.
|
|
class Communicator < Vagrant.plugin("2", :communicator)
|
|
def initialize(machine)
|
|
@machine = machine
|
|
@host_vm = machine.provider.host_vm
|
|
|
|
# We only work on the Docker provider
|
|
if machine.provider_name != :docker
|
|
raise Errors::CommunicatorNotDocker
|
|
end
|
|
end
|
|
|
|
#-------------------------------------------------------------------
|
|
# Communicator Methods
|
|
#-------------------------------------------------------------------
|
|
|
|
def ready?
|
|
# We can't be ready if we can't talk to the host VM
|
|
return false if !@host_vm.communicate.ready?
|
|
|
|
# We're ready if we can establish an SSH connection to the container
|
|
command = container_ssh_command
|
|
return false if !command
|
|
@host_vm.communicate.test("#{command} exit")
|
|
end
|
|
|
|
def download(from, to)
|
|
# Same process as upload, but in reverse
|
|
|
|
# First, we use `cat` to copy that file from the Docker container.
|
|
temp = "/tmp/docker_d#{Time.now.to_i}_#{rand(100000)}"
|
|
@host_vm.communicate.execute("#{container_ssh_command} 'cat #{from}' >#{temp}")
|
|
|
|
# Then, we download this from the host VM.
|
|
@host_vm.communicate.download(temp, to)
|
|
|
|
# Remove the temporary file
|
|
@host_vm.communicate.execute("rm -f #{temp}", error_check: false)
|
|
end
|
|
|
|
def execute(command, **opts, &block)
|
|
fence = {}
|
|
fence[:stderr] = "VAGRANT FENCE: #{Time.now.to_i} #{rand(100000)}"
|
|
fence[:stdout] = "VAGRANT FENCE: #{Time.now.to_i} #{rand(100000)}"
|
|
|
|
# We want to emulate how the SSH communicator actually executes
|
|
# things, so we build up the list of commands to execute in a
|
|
# giant shell script.
|
|
tf = Tempfile.new("vagrant")
|
|
tf.binmode
|
|
tf.write("export TERM=vt100\n")
|
|
tf.write("echo #{fence[:stdout]}\n")
|
|
tf.write("echo #{fence[:stderr]} >&2\n")
|
|
tf.write("#{command}\n")
|
|
tf.write("exit\n")
|
|
tf.close
|
|
|
|
# Upload the temp file to the remote machine
|
|
remote_temp = "/tmp/docker_#{Time.now.to_i}_#{rand(100000)}"
|
|
@host_vm.communicate.upload(tf.path, remote_temp)
|
|
|
|
# Determine the shell to execute. Prefer the explicitly passed in shell
|
|
# over the default configured shell. If we are using `sudo` then we
|
|
# need to wrap the shell in a `sudo` call.
|
|
shell_cmd = @machine.config.ssh.shell
|
|
shell_cmd = opts[:shell] if opts[:shell]
|
|
shell_cmd = "sudo -E -H #{shell_cmd}" if opts[:sudo]
|
|
|
|
acc = {}
|
|
fenced = {}
|
|
result = @host_vm.communicate.execute(
|
|
"#{container_ssh_command} '#{shell_cmd}' <#{remote_temp}",
|
|
opts) do |type, data|
|
|
# If we don't have a block, we don't care about the data
|
|
next if !block
|
|
|
|
# We only care about stdout and stderr output
|
|
next if ![:stdout, :stderr].include?(type)
|
|
|
|
# If we reached our fence, then just output
|
|
if fenced[type]
|
|
block.call(type, data)
|
|
next
|
|
end
|
|
|
|
# Otherwise, accumulate
|
|
acc[type] = data
|
|
|
|
# Look for the fence
|
|
index = acc[type].index(fence[type])
|
|
next if !index
|
|
|
|
fenced[type] = true
|
|
index += fence[type].length
|
|
data = acc[type][index..-1].chomp
|
|
acc[type] = ""
|
|
block.call(type, data)
|
|
end
|
|
|
|
@host_vm.communicate.execute("rm -f #{remote_temp}", error_check: false)
|
|
|
|
return result
|
|
end
|
|
|
|
def sudo(command, **opts, &block)
|
|
opts = { sudo: true }.merge(opts)
|
|
execute(command, opts, &block)
|
|
end
|
|
|
|
def test(command, **opts)
|
|
opts = { error_check: false }.merge(opts)
|
|
execute(command, opts) == 0
|
|
end
|
|
|
|
def upload(from, to)
|
|
# First, we upload this to the host VM to some temporary directory.
|
|
to_temp = "/tmp/docker_#{Time.now.to_i}_#{rand(100000)}"
|
|
@host_vm.communicate.upload(from, to_temp)
|
|
|
|
# Then, we use `cat` to get that file into the Docker container.
|
|
@host_vm.communicate.execute(
|
|
"#{container_ssh_command} 'cat >#{to}' <#{to_temp}")
|
|
|
|
# Remove the temporary file
|
|
@host_vm.communicate.execute("rm -f #{to_temp}", error_check: false)
|
|
end
|
|
|
|
#-------------------------------------------------------------------
|
|
# Other Methods
|
|
#-------------------------------------------------------------------
|
|
|
|
# This returns the raw SSH command string that can be used to
|
|
# connect via SSH to the container if you're on the same machine
|
|
# as the container.
|
|
#
|
|
# @return [String]
|
|
def container_ssh_command
|
|
# Get the container's SSH info
|
|
info = @machine.ssh_info
|
|
return nil if !info
|
|
info[:port] ||= 22
|
|
|
|
# Make sure our private keys are synced over to the host VM
|
|
ssh_args = sync_private_keys(info).map do |path|
|
|
"-i #{path}"
|
|
end
|
|
|
|
# Use ad-hoc SSH options for the hop on the docker proxy
|
|
if info[:forward_agent]
|
|
ssh_args << "-o ForwardAgent=yes"
|
|
end
|
|
ssh_args.concat(["-o Compression=yes",
|
|
"-o ConnectTimeout=5",
|
|
"-o StrictHostKeyChecking=no",
|
|
"-o UserKnownHostsFile=/dev/null"])
|
|
|
|
# Build the SSH command
|
|
"ssh #{info[:username]}@#{info[:host]} -p#{info[:port]} #{ssh_args.join(" ")}"
|
|
end
|
|
|
|
protected
|
|
|
|
def sync_private_keys(info)
|
|
@keys ||= {}
|
|
|
|
id = Digest::MD5.hexdigest(
|
|
@machine.env.root_path.to_s + @machine.name.to_s)
|
|
|
|
result = []
|
|
info[:private_key_path].each do |path|
|
|
if !@keys[path.to_s]
|
|
# We haven't seen this before, upload it!
|
|
guest_path = "/tmp/key_#{id}_#{Digest::MD5.hexdigest(path.to_s)}"
|
|
@host_vm.communicate.upload(path.to_s, guest_path)
|
|
|
|
# Make sure it has the proper chmod
|
|
@host_vm.communicate.execute("chmod 0600 #{guest_path}")
|
|
|
|
# Set it
|
|
@keys[path.to_s] = guest_path
|
|
end
|
|
|
|
result << @keys[path.to_s]
|
|
end
|
|
|
|
result
|
|
end
|
|
end
|
|
end
|
|
end
|