Skip to content
This repository was archived by the owner on Jul 6, 2018. It is now read-only.

Docker toolbox and chef-zero support #81

Merged
merged 5 commits into from
Mar 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 0 additions & 95 deletions lib/chef/provisioning/docker_driver/chef_zero_http_proxy.rb

This file was deleted.

141 changes: 91 additions & 50 deletions lib/chef/provisioning/docker_driver/docker_transport.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
require 'chef/provisioning/transport'
require 'chef/provisioning/transport/ssh'
require 'docker'
require 'archive/tar/minitar'
require 'shellwords'
require 'uri'
require 'socket'
require 'mixlib/shellout'
require 'sys/proctable'
require 'chef/provisioning/docker_driver/chef_zero_http_proxy'
require 'tempfile'

class Chef
module Provisioning
Expand All @@ -21,14 +22,13 @@ def initialize(container, config)
attr_accessor :container

def execute(command, options={})
Chef::Log.debug("execute '#{command}' with options #{options}")

opts = {}
if options[:keep_stdin_open]
opts[:stdin] = true
end

command = Shellwords.split(command) if command.is_a?(String)
Chef::Log.debug("execute #{command.inspect} on container #{container.id} with options #{opts}'")
response = container.exec(command, opts) do |stream, chunk|
case stream
when :stdout
Expand All @@ -47,9 +47,11 @@ def read_file(path)
begin
tarfile = ''
# NOTE: this would be more efficient if we made it a stream and passed that to Minitar
container.copy(path) do |block|
container.archive_out(path) do |block|
tarfile << block
end
rescue Docker::Error::NotFoundError
return nil
rescue Docker::Error::ServerError
if $!.message =~ /500/ || $!.message =~ /Could not find the file/
return nil
Expand All @@ -72,11 +74,11 @@ def read_file(path)
end

def write_file(path, content)
File.open(container_path(path), 'w') { |file| file.write(content) }
tar = StringIO.new(Docker::Util.create_tar(path => content))
container.archive_in_stream('/') { tar.read }
end

def download_file(path, local_path)
# TODO stream
file = File.open(local_path, 'w')
begin
file.write(read_file(path))
Expand All @@ -87,71 +89,110 @@ def download_file(path, local_path)
end

def upload_file(local_path, path)
FileUtils.cp(local_path, container_path(path))
write_file(path, IO.read(local_path))
end

def make_url_available_to_remote(url)
# The host is already open to the container. Just find out its address and return it!
uri = URI(url)
uri.scheme = 'http' if 'chefzero' == uri.scheme && uri.host == 'localhost'
host = Socket.getaddrinfo(uri.host, uri.scheme, nil, :STREAM)[0][3]
Chef::Log.debug("Making URL available: #{host}")

if host == '127.0.0.1' || host == '::1'
result = execute('ip route list', :read_only => true)

Chef::Log.debug("IP route: #{result.stdout}")

if result.stdout =~ /default via (\S+)/

uri.host = if using_boot2docker?
# Intermediate VM does NAT, so local address should be fine here
Chef::Log.debug("Using boot2docker!")
IPSocket.getaddress(Socket.gethostname)
else
$1
end

if !@proxy_thread
# Listen to docker instances only, and forward to localhost
@proxy_thread = Thread.new do
Chef::Log.debug("Starting proxy thread: #{uri.host}:#{uri.port} <--> #{host}:#{uri.port}")
ChefZeroHttpProxy.new(uri.host, uri.port, host, uri.port).run
def make_url_available_to_remote(local_url)
uri = URI(local_url)

if uri.scheme == "chefzero" || is_local_machine(uri.host)
# chefzero: URLs are just http URLs with a shortcut if you are in-process.
# The remote machine is definitely not in-process.
uri.scheme = "http" if uri.scheme == "chefzero"

if docker_toolkit_transport
# Forward localhost on docker_machine -> chef-zero. The container will
# be able to access this because it was started with --net=host.
uri = docker_toolkit_transport.make_url_available_to_remote(uri.to_s)
uri = URI(uri)
@docker_toolkit_transport_thread ||= Thread.new do
begin
docker_toolkit_transport.send(:session).loop { true }
rescue
Chef::Log.error("SSH forwarding loop failed: #{$!}")
raise
end
Chef::Log.debug("Session loop completed normally")
end
Chef::Log.debug("Using Chef server URL: #{uri.to_s}")

return uri.to_s
else
raise "Cannot forward port: ip route ls did not show default in expected format.\nSTDOUT: #{result.stdout}"
# We are the host. The docker machine was run with --net=host, so it
# will be able to talk to us automatically.
end
else
old_uri = uri.dup
# Find out our external network address of the URL and report it
# to the container in case it has no DNS (often the case).
uri.scheme = 'http' if 'chefzero' == uri.scheme && uri.host == 'localhost'
uri.host = Socket.getaddrinfo(uri.host, uri.scheme, nil, :STREAM)[0][3]
Chef::Log.debug("Looked up IP address of #{old_uri} and modified URL to point at it: #{uri}")
end
url

uri.to_s
end

def disconnect
@proxy_thread.kill if @proxy_thread
if @docker_toolkit_transport_thread
@docker_toolkit_transport_thread.kill
@docker_toolkit_transport_thread = nil
end
end

def available?
end

private

# boot2docker introduces an intermediate VM so we need to use a slightly different
# mechanism for getting to the running chef-zero
def using_boot2docker?
Sys::ProcTable.ps do |proc|
if proc.respond_to?(:cmdline)
if proc.send(:cmdline).to_s =~ /.*--comment boot2docker.*/
return true
end
def is_local_machine(host)
local_addrs = Socket.ip_address_list
host_addrs = Addrinfo.getaddrinfo(host, nil)
local_addrs.any? do |local_addr|
host_addrs.any? do |host_addr|
local_addr.ip_address == host_addr.ip_address
end
end
end

def container_path(path)
File.join('proc', container.info['State']['Pid'].to_s, 'root', path)
def docker_toolkit_transport
if !defined?(@docker_toolkit_transport)
# Figure out which docker-machine this container is in
begin
docker_machines = `docker-machine ls --format "{{.Name}},{{.URL}}"`
rescue Errno::ENOENT
Chef::Log.debug("docker-machine ls returned ENOENT: Docker Toolkit is presumably not installed.")
@docker_toolkit_transport = nil
return
end
Chef::Log.debug("Found docker machines:")
docker_machine = nil
docker_machines.lines.each do |line|
machine_name, machine_url = line.chomp.split(',', 2)
Chef::Log.debug("- #{machine_name} at URL #{machine_url.inspect}")
if machine_url == container.connection.url
Chef::Log.debug("Docker machine #{machine_name} at URL #{machine_url} matches the container's URL #{container.connection.url}! Will use it for port forwarding.")
docker_machine = machine_name
end
end
if !docker_machine
Chef::Log.debug("Docker Toolkit is installed, but no Docker machine's URL matches #{container.connection.url.inspect}. Assuming docker must be installed as well ...")
@docker_toolkit_transport = nil
return
end

# Get the SSH information for the docker-machine
docker_toolkit_json = `docker-machine inspect #{docker_machine}`
machine_info = JSON.parse(docker_toolkit_json, create_additions: false)["Driver"]
ssh_host = machine_info["IPAddress"]
ssh_username = machine_info["SSHUser"]
ssh_options = {
# port: machine_info["SSHPort"], seems to be bad information (44930???)
keys: [ machine_info["SSHKeyPath"] ],
keys_only: true
}

Chef::Log.debug("Docker Toolkit is installed. Will use SSH transport with docker-machine #{docker_machine.inspect} to perform port forwarding.")
@docker_toolkit_transport = Chef::Provisioning::Transport::SSH.new(ssh_host, ssh_username, ssh_options, {}, Chef::Config)
end
@docker_toolkit_transport
end

class DockerResult
Expand Down
33 changes: 29 additions & 4 deletions lib/chef/provisioning/docker_driver/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def allocate_machine(action_handler, machine_spec, machine_options)
'host_node' => action_handler.host_node,
'container_name' => container_name,
'image_id' => image_id,
'docker_options' => docker_options,
'docker_options' => stringize_keys(docker_options),
'container_id' => container_id
}
build_container(machine_spec, docker_options)
Expand Down Expand Up @@ -123,7 +123,13 @@ def build_container(machine_spec, docker_options)
args << '-i'
end

if docker_options[:env]
# We create the initial container with --net host so it can access things
# while it converges. When the final container starts, it will have its
# normal network.
args << '--net'
args << 'host'

if docker_options[:env]
docker_options[:env].each do |key, value|
args << '-e'
args << "#{key}=#{value}"
Expand All @@ -144,8 +150,20 @@ def build_container(machine_spec, docker_options)
end
end

if docker_options[:dns]
docker_options[:dns].each do |entry|
args << '--dns'
args << "#{entry}"
end
end

if docker_options[:dns_search]
args << '--dns-search'
args << "#{docker_options[:dns_search]}"
end

args << image.id
args += Shellwords.split("/bin/sh -c 'while true;do sleep 1; done'")
args += Shellwords.split("/bin/sh -c 'while true;do sleep 1000; done'")

cmdstr = Shellwords.join(args)
Chef::Log.debug("Executing #{cmdstr}")
Expand Down Expand Up @@ -173,7 +191,7 @@ def build_image(machine_spec, docker_options)
'repo' => source_repository,
'tag' => source_tag
)

Chef::Log.debug("Allocated #{image}")
image.tag('repo' => 'chef', 'tag' => target_tag)
Chef::Log.debug("Tagged image #{image}")
Expand Down Expand Up @@ -327,6 +345,13 @@ def base_image_for(machine_spec)
image_spec = machine_spec.managed_entry_store.get!(:machine_image, machine_spec.from_image)
Mash.new(image_spec.reference)[:docker_options][:base_image]
end

def stringize_keys(hash)
hash.each_with_object({}) do |(k,v),hash|
v = stringize_keys(v) if v.is_a?(Hash)
hash[k.to_s] = v
end
end
end
end
end
Expand Down