From 07753c4520d3364124dbdca434cd102e471b8bb4 Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Sat, 20 Jun 2015 01:09:41 -0700 Subject: [PATCH 1/2] leverage exec for transport and implement connect_to_machine --- .gitignore | 13 - README.md | 11 +- .../docker_driver/docker_container_machine.rb | 31 +- .../docker_driver/docker_transport.rb | 181 ++---------- lib/chef/provisioning/docker_driver/driver.rb | 270 +++++++++++------- 5 files changed, 201 insertions(+), 305 deletions(-) diff --git a/.gitignore b/.gitignore index f430f5f..d87d4be 100644 --- a/.gitignore +++ b/.gitignore @@ -15,16 +15,3 @@ spec/reports test/tmp test/version_tmp tmp -*.bundle -*.so -*.o -*.a -mkmf.log -*.sw* -clients -nodes -docs/examples/clients -docs/examples/nodes -docs/examples/data_bags -x.rb -.idea/ diff --git a/README.md b/README.md index 4fb451f..6c80f89 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,8 @@ This supports the new machine image paradigm; with Docker you can build a base ```ruby require 'chef/provisioning/docker_driver' -machine_image 'web_server' do - recipe 'apache' +machine_image 'ssh_server' do + recipe 'openssh' machine_options :docker_options => { :base_image => { @@ -84,11 +84,12 @@ machine_image 'web_server' do } end -machine 'web00' do - from_image 'web_server' +machine 'ssh00' do + from_image 'ssh_server' machine_options :docker_options => { - :command => '/usr/sbin/httpd' + :command => '/usr/sbin/sshd -D -o UsePAM=no -o UsePrivilegeSeparation=no -o PidFile=/tmp/sshd.pid', + :ports => [22] } end ``` diff --git a/lib/chef/provisioning/docker_driver/docker_container_machine.rb b/lib/chef/provisioning/docker_driver/docker_container_machine.rb index 53de04b..45b06b6 100644 --- a/lib/chef/provisioning/docker_driver/docker_container_machine.rb +++ b/lib/chef/provisioning/docker_driver/docker_container_machine.rb @@ -9,29 +9,30 @@ class DockerContainerMachine < Chef::Provisioning::Machine::UnixMachine # Options is expected to contain the optional keys # :command => the final command to execute # :ports => a list of port numbers to listen on - def initialize(machine_spec, transport, convergence_strategy, opts = {}) + def initialize(machine_spec, transport, convergence_strategy, command = nil) super(machine_spec, transport, convergence_strategy) - @env = opts[:env] - @command = opts[:command] - @ports = opts[:ports] - @volumes = opts[:volumes] - @keep_stdin_open = opts[:keep_stdin_open] - @container_name = machine_spec.location['container_name'] + @command = command @transport = transport end - def execute_always(command, options = {}) - transport.execute(command, { :read_only => true }.merge(options)) - end - def converge(action_handler) super action_handler - if @command - Chef::Log.debug("DockerContainerMachine converge complete, executing #{@command} in #{@container_name}") - @transport.execute(@command, :env => @env ,:detached => true, :read_only => true, :ports => @ports, :volumes => @volumes, :keep_stdin_open => @keep_stdin_open) + Chef::Log.debug("DockerContainerMachine converge complete, executing #{@command} in #{@container_name}") + image = transport.container.commit( + 'repo' => 'chef', + 'tag' => machine_spec.reference['container_name'] + ) + machine_spec.reference['image_id'] = image.id + + if @command && transport.container.info['Config']['Cmd'].join(' ') != @command + transport.container.delete(:force => true) + container = image.run(Shellwords.split(@command)) + container.rename(machine_spec.reference['container_name']) + machine_spec.reference['container_id'] = container.id + transport.container = container end + machine_spec.save(action_handler) end - end end end diff --git a/lib/chef/provisioning/docker_driver/docker_transport.rb b/lib/chef/provisioning/docker_driver/docker_transport.rb index dcc0b6c..61aeed6 100644 --- a/lib/chef/provisioning/docker_driver/docker_transport.rb +++ b/lib/chef/provisioning/docker_driver/docker_transport.rb @@ -12,117 +12,38 @@ class Chef module Provisioning module DockerDriver class DockerTransport < Chef::Provisioning::Transport - def initialize(container_name, base_image_name, credentials, connection, tunnel_transport = nil) - @repository_name = 'chef' - @container_name = container_name - @image = Docker::Image.get(base_image_name, connection) - @credentials = credentials - @connection = connection - @tunnel_transport = tunnel_transport + def initialize(container, config) + @container = container + @config = config end - include Chef::Mixin::ShellOut + attr_reader :config + attr_accessor :container - attr_reader :container_name - attr_reader :repository_name - attr_reader :image - attr_reader :credentials - attr_reader :connection - attr_reader :tunnel_transport - - # Execute the specified command inside the container, returns a Mixlib::Shellout object - # Options contains the optional keys: - # :env => env vars - # :read_only => Do not commit this execute operation, just execute it - # :ports => ports to listen on (-p command-line options) - # :detached => true/false, execute this command in detached mode (for final program to run) def execute(command, options={}) Chef::Log.debug("execute '#{command}' with options #{options}") - begin - connection.post("/containers/#{container_name}/stop?t=0", '') - Chef::Log.debug("stopped /containers/#{container_name}") - rescue Excon::Errors::NotModified - Chef::Log.debug("Already stopped #{container_name}") - rescue Docker::Error::NotFoundError - end - - begin - # Delete the container if it exists and is dormant - connection.delete("/containers/#{container_name}?v=true&force=true") - Chef::Log.debug("deleted /containers/#{container_name}") - rescue Docker::Error::NotFoundError + opts = {} + if options[:keep_stdin_open] + opts[:stdin] = true end command = Shellwords.split(command) if command.is_a?(String) - - # TODO shell_out has no way to live stream stderr??? - live_stream = nil - live_stream = STDOUT if options[:stream] - live_stream = options[:stream_stdout] if options[:stream_stdout] - - args = ['docker', 'run', '--name', container_name] - - if options[:env] - options[:env].each do |key, value| - args << '-e' - args << "#{key}=#{value}" - end - end - - if options[:detached] - args << '--detach' - end - - if options[:ports] - options[:ports].each do |portnum| - args << '-p' - args << "#{portnum}" - end - end - - if options[:volumes] - options[:volumes].each do |volume| - args << '-v' - args << "#{volume}" + response = container.exec(command, opts) do |stream, chunk| + case stream + when :stdout + stream_chunk(options, chunk, nil) + when :stderr + stream_chunk(options, nil, chunk) end end - if options[:keep_stdin_open] - args << '-i' - end - - args << @image.id - args += command + Chef::Log.debug("Execute complete: status #{response[2]}") - cmdstr = Shellwords.join(args) - Chef::Log.debug("Executing #{cmdstr}") - - # Remove this when https://github.com/opscode/chef/pull/2100 gets merged and released - # nullify live_stream because at the moment EventsOutputStream doesn't understand <<, which - # ShellOut uses - live_stream = nil unless live_stream.respond_to? :<< - - cmd = Mixlib::ShellOut.new(cmdstr, :live_stream => live_stream, :timeout => execute_timeout(options)) - - cmd.run_command - - unless options[:read_only] - Chef::Log.debug("Committing #{container_name} as #{repository_name}:#{container_name}") - container = Docker::Container.get(container_name) - @image = container.commit('repo' => repository_name, 'tag' => container_name) - end - - Chef::Log.debug("Execute complete: status #{cmd.exitstatus}") - - cmd + DockerResult.new(command.join(' '), options, response[0].join, response[1].join, response[2]) end def read_file(path) - container = Docker::Container.create({ - 'Image' => @image.id, - 'Cmd' => %w(echo true) - }, connection) begin tarfile = '' # NOTE: this would be more efficient if we made it a stream and passed that to Minitar @@ -135,8 +56,6 @@ def read_file(path) else raise end - ensure - container.delete end output = '' @@ -153,12 +72,7 @@ def read_file(path) end def write_file(path, content) - # TODO hate tempfiles. Find an in memory way. - Tempfile.open('metal_docker_write_file') do |file| - file.write(content) - file.close - @image = @image.insert_local('localPath' => file.path, 'outputPath' => path, 't' => "#{repository_name}:#{container_name}") - end + File.open(container_path(path), 'w') { |file| file.write(content) } end def download_file(path, local_path) @@ -173,7 +87,7 @@ def download_file(path, local_path) end def upload_file(local_path, path) - @image = @image.insert_local('localPath' => local_path, 'outputPath' => path, 't' => "#{repository_name}:#{container_name}") + FileUtils.cp(local_path, container_path(path)) end def make_url_available_to_remote(url) @@ -236,40 +150,8 @@ def using_boot2docker? end end - # Copy of container.attach with timeout support and pipeline - def attach_with_timeout(container, read_timeout, options = {}, &block) - opts = { - :stream => true, :stdout => true, :stderr => true - }.merge(options) - # Creates list to store stdout and stderr messages - msgs = Docker::Messages.new - connection.start_request( - :post, - "/containers/#{container.id}/attach", - opts, - :response_block => attach_for(block, msgs), - :read_timeout => read_timeout, - :pipeline => true, - :persistent => true - ) - end - - # Method that takes chunks and calls the attached block for each mux'd message - def attach_for(block, msg_stack) - messages = Docker::Messages.new - lambda do |c,r,t| - messages = messages.decipher_messages(c) - msg_stack.append(messages) - - unless block.nil? - messages.stdout_messages.each do |msg| - block.call(:stdout, msg) - end - messages.stderr_messages.each do |msg| - block.call(:stderr, msg) - end - end - end + def container_path(path) + File.join('proc', container.info['State']['Pid'].to_s, 'root', path) end class DockerResult @@ -300,26 +182,3 @@ def error! end end end - -class Docker::Connection - def start_request(method, *args, &block) - request = compile_request_params(method, *args, &block) - if Docker.logger - Docker.logger.debug( - [request[:method], request[:path], request[:query], request[:body]] - ) - end - excon = resource - [ excon, excon.request(request) ] - rescue Excon::Errors::BadRequest => ex - raise ClientError, ex.message - rescue Excon::Errors::Unauthorized => ex - raise UnauthorizedError, ex.message - rescue Excon::Errors::NotFound => ex - raise NotFoundError, ex.message - rescue Excon::Errors::InternalServerError => ex - raise ServerError, ex.message - rescue Excon::Errors::Timeout => ex - raise TimeoutError, ex.message - end -end diff --git a/lib/chef/provisioning/docker_driver/driver.rb b/lib/chef/provisioning/docker_driver/driver.rb index cdd40a5..61f3495 100644 --- a/lib/chef/provisioning/docker_driver/driver.rb +++ b/lib/chef/provisioning/docker_driver/driver.rb @@ -72,64 +72,112 @@ def self.connection_url(driver_url) def allocate_machine(action_handler, machine_spec, machine_options) + machine_spec.from_image = action_handler.provider.new_resource.from_image + docker_options = machine_options[:docker_options] + container_id = nil + image_id = machine_options[:image_id] + if machine_spec.reference + container_name = machine_spec.reference['container_name'] + container_id = machine_spec.reference['container_id'] + image_id ||= machine_spec.reference['image_id'] + docker_options ||= machine_spec.reference['docker_options'] + end - container_name = machine_spec.name + container_name ||= machine_spec.name machine_spec.reference = { - 'driver_url' => driver_url, - 'driver_version' => Chef::Provisioning::DockerDriver::VERSION, - 'allocated_at' => Time.now.utc.to_s, - 'host_node' => action_handler.host_node, - 'container_name' => container_name, - 'image_id' => machine_options[:image_id], - 'docker_options' => machine_options[:docker_options] + 'driver_url' => driver_url, + 'driver_version' => Chef::Provisioning::DockerDriver::VERSION, + 'allocated_at' => Time.now.utc.to_s, + 'host_node' => action_handler.host_node, + 'container_name' => container_name, + 'image_id' => image_id, + 'docker_options' => docker_options, + 'container_id' => container_id } + build_container(machine_spec, docker_options) end def ready_machine(action_handler, machine_spec, machine_options) - base_image_name = build_container(machine_spec, machine_options) - start_machine(action_handler, machine_spec, machine_options, base_image_name) - machine_for(machine_spec, machine_options, base_image_name) + start_machine(action_handler, machine_spec, machine_options) + machine_for(machine_spec, machine_options) end - def build_container(machine_spec, machine_options) - docker_options = machine_options[:docker_options] + def build_container(machine_spec, docker_options) + container = container_for(machine_spec) + return container unless container.nil? + + image = find_image(machine_spec) || + build_image(machine_spec, docker_options) + + args = [ + 'docker', + 'run', + '--name', + machine_spec.reference['container_name'], + '--detach' + ] - base_image = docker_options[:base_image] - if !base_image - Chef::Log.debug("No base images specified in docker options.") - base_image = base_image_for(machine_spec) + if docker_options[:keep_stdin_open] + args << '-i' end + + if docker_options[:env] + docker_options[:env].each do |key, value| + args << '-e' + args << "#{key}=#{value}" + end + end + + if docker_options[:ports] + docker_options[:ports].each do |portnum| + args << '-p' + args << "#{portnum}" + end + end + + if docker_options[:volumes] + docker_options[:volumes].each do |volume| + args << '-v' + args << "#{volume}" + end + end + + args << image.id + args += Shellwords.split("/bin/sh -c 'while true;do sleep 1; done'") + + cmdstr = Shellwords.join(args) + Chef::Log.debug("Executing #{cmdstr}") + + cmd = Mixlib::ShellOut.new(cmdstr) + cmd.run_command + + container = Docker::Container.get(machine_spec.reference['container_name']) + + Chef::Log.debug("Container id: #{container.id}") + machine_spec.reference['container_id'] = container.id + container + end + + def build_image(machine_spec, docker_options) + base_image = docker_options[:base_image] || base_image_for(machine_spec) source_name = base_image[:name] source_repository = base_image[:repository] source_tag = base_image[:tag] - # Don't do this if we're loading from an image - if docker_options[:from_image] - "#{source_repository}:#{source_tag}" - else - target_repository = 'chef' - target_tag = machine_spec.name - - # check if target image exists, if not try to look up for source image. - image = find_image(target_repository, target_tag) || find_image(source_repository, source_tag) - - # kick off image creation - if image == nil - Chef::Log.debug("No matching images for #{target_repository}:#{target_tag}, creating!") - image = Docker::Image.create('fromImage' => source_name, - 'repo' => source_repository , - 'tag' => source_tag) - Chef::Log.debug("Allocated #{image}") - image.tag('repo' => 'chef', 'tag' => target_tag) - Chef::Log.debug("Tagged image #{image}") - elsif not image.info['RepoTags'].include? "#{target_repository}:#{target_tag}" - # if `find_image(source_repository, source_tag)` returned result, assign target tag to it to be able - # find it in `start_machine`. - image.tag('repo' => target_repository, 'tag' => target_tag) - end + target_tag = machine_spec.reference['container_name'] - "#{target_repository}:#{target_tag}" - end + image = Docker::Image.create( + 'fromImage' => source_name, + 'repo' => source_repository, + 'tag' => source_tag + ) + + Chef::Log.debug("Allocated #{image}") + image.tag('repo' => 'chef', 'tag' => target_tag) + Chef::Log.debug("Tagged image #{image}") + + machine_spec.reference['image_id'] = image.id + image end def allocate_image(action_handler, image_spec, image_options, machine_spec, machine_options) @@ -163,101 +211,101 @@ def destroy_image(action_handler, image_spec, image_options, machine_options={}) # Connect to machine without acquiring it def connect_to_machine(machine_spec, machine_options) - Chef::Log.debug('Connect to machine!') + Chef::Log.debug('Connect to machine') + machine_for(machine_spec, machine_options) end def destroy_machine(action_handler, machine_spec, machine_options) - container_name = machine_spec.location['container_name'] - Chef::Log.debug("Destroying container: #{container_name}") - container = Docker::Container.get(container_name, @connection) - - begin - Chef::Log.debug("Stopping #{container_name}") - container.stop - rescue Excon::Errors::NotModified - # this is okay - Chef::Log.debug('Already stopped!') + container = container_for(machine_spec) + if container + Chef::Log.debug("Destroying container: #{container.id}") + container.delete(:force => true) end - Chef::Log.debug("Removing #{container_name}") - container.delete - if !machine_spec.attrs[:keep_image] && !machine_options[:keep_image] - Chef::Log.debug("Destroying image: chef:#{container_name}") - image = Docker::Image.get("chef:#{container_name}") + image = find_image(machine_spec) + Chef::Log.debug("Destroying image: chef:#{image.id}") image.delete end end - def stop_machine(action_handler, node) - Chef::Log.debug("Stop machine: #{node.inspect}") - end + def stop_machine(action_handler, machine_spec, machine_options) + container = container_for(machine_spec) + return if container.nil? - def image_named(image_name) - Docker::Image.all.select { - |i| i.info['RepoTags'].include? image_name - }.first + container.stop if container.info['State']['Running'] end - def find_image(repository, tag) - Docker::Image.all.select { - |i| i.info['RepoTags'].include? "#{repository}:#{tag}" - }.first + def find_image(machine_spec) + image = nil + + if machine_spec.reference['image_id'] + begin + image = Docker::Image.get(machine_spec.reference['image_id']) + rescue Docker::Error::NotFoundError + end + end + + if image.nil? + image_name = "chef:#{machine_spec.reference['container_name']}" + if machine_spec.from_image + base_image = base_image_for(machine_spec) + image_name = "#{base_image[:repository]}:#{base_image[:tag]}" + end + + image = Docker::Image.all.select { + |i| i.info['RepoTags'].include? image_name + }.first + + if machine_spec.from_image && image.nil? + raise "Unable to locate machine_image for #{image_name}" + end + end + + machine_spec.reference['image_id'] = image.id if image + + image end def driver_url "docker:#{Docker.url}" end - def start_machine(action_handler, machine_spec, machine_options, base_image_name) - # Spin up a docker instance if needed, otherwise use the existing one - container_name = machine_spec.location['container_name'] - - begin - Docker::Container.get(container_name, @connection) - rescue Docker::Error::NotFoundError - docker_options = machine_options[:docker_options] - Chef::Log.debug("Start machine for container #{container_name} using base image #{base_image_name} with options #{docker_options.inspect}") - image = image_named(base_image_name) - container = Docker::Container.create('Image' => image.id, 'name' => container_name) - Chef::Log.debug("Container id: #{container.id}") - machine_spec.location['container_id'] = container.id + def start_machine(action_handler, machine_spec, machine_options) + container = container_for(machine_spec) + if container && !container.info['State']['Running'] + container.start end - end - def machine_for(machine_spec, machine_options, base_image_name) + def machine_for(machine_spec, machine_options) Chef::Log.debug('machine_for...') + docker_options = machine_options[:docker_options] || Mash.from_hash(machine_spec.reference['docker_options']) - docker_options = machine_options[:docker_options] + container = Docker::Container.get(machine_spec.reference['container_id'], @connection) - transport = DockerTransport.new(machine_spec.location['container_name'], - base_image_name, - nil, - Docker.connection) - - convergence_strategy = if docker_options[:from_image] - Chef::Provisioning::ConvergenceStrategy::NoConverge.new({}, config) - else - convergence_strategy_for(machine_spec, machine_options) - end - - Chef::Provisioning::DockerDriver::DockerContainerMachine.new( - machine_spec, - transport, - convergence_strategy, - :command => docker_options[:command], - :env => docker_options[:env], - :ports => Array(docker_options[:ports]), - :volumes => Array(docker_options[:volumes]), - :keep_stdin_open => docker_options[:keep_stdin_open] - ) + if machine_spec.from_image + convergence_strategy = Chef::Provisioning::ConvergenceStrategy::NoConverge.new({}, config) + else + convergence_strategy = Chef::Provisioning::ConvergenceStrategy::InstallCached. + new(machine_options[:convergence_options], config) + end + + transport = DockerTransport.new(container, config) + + Chef::Provisioning::DockerDriver::DockerContainerMachine.new( + machine_spec, + transport, + convergence_strategy, + docker_options[:command] + ) end - def convergence_strategy_for(machine_spec, machine_options) - @unix_convergence_strategy ||= begin - Chef::Provisioning::ConvergenceStrategy::InstallCached. - new(machine_options[:convergence_options], config) + def container_for(machine_spec) + container_id = machine_spec.reference['container_id'] + begin + container = Docker::Container.get(container_id, @connection) if container_id + rescue Docker::Error::NotFoundError end end From 977ade166c1e7ecad88145184114f1b4655b4930 Mon Sep 17 00:00:00 2001 From: Matt Wrock Date: Sat, 20 Jun 2015 18:54:29 -0700 Subject: [PATCH 2/2] fix machine allocate from_image inside of a machine_batch --- lib/chef/provisioning/docker_driver/driver.rb | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/chef/provisioning/docker_driver/driver.rb b/lib/chef/provisioning/docker_driver/driver.rb index 61f3495..21d2e17 100644 --- a/lib/chef/provisioning/docker_driver/driver.rb +++ b/lib/chef/provisioning/docker_driver/driver.rb @@ -70,9 +70,11 @@ def self.connection_url(driver_url) end end - def allocate_machine(action_handler, machine_spec, machine_options) - machine_spec.from_image = action_handler.provider.new_resource.from_image + machine_spec.from_image = from_image_from_action_handler( + action_handler, + machine_spec + ) docker_options = machine_options[:docker_options] container_id = nil image_id = machine_options[:image_id] @@ -267,6 +269,17 @@ def find_image(machine_spec) image end + def from_image_from_action_handler(action_handler, machine_spec) + case action_handler + when Chef::Provisioning::AddPrefixActionHandler + machines = action_handler.action_handler.provider.new_resource.machines + this_machine = machines.select { |m| m.name == machine_spec.name}.first + this_machine.from_image + else + action_handler.provider.new_resource.from_image + end + end + def driver_url "docker:#{Docker.url}" end